Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
# .env is NOT checked into the repository.
# ============================================================

# --- Docker Registry --------------------------------------------
# URL of the private registry (without https://)
REGISTRY_URL=registry.example.com

# Registry credentials (prompted for by the setup script,
# but can also be hardcoded here)
REGISTRY_USER=
REGISTRY_PASSWORD=
# --- Docker Image (optional) --------------------------------------------
# The default image is the public ghcr.io/lukislp/studylife-server image built by this
# repo's own CI/CD pipeline - no registry login needed to pull it.
# Only set SERVER_IMAGE (and, if it's on a private registry, the REGISTRY_* variables
# below) if you're running your own fork published to your own registry.
# SERVER_IMAGE=registry.example.com/your-fork/studylife-server:latest
# REGISTRY_URL=registry.example.com
# REGISTRY_USER=
# REGISTRY_PASSWORD=

# --- App ----------------------------------------------------
# Port the app is reachable on
Expand Down
49 changes: 26 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,14 @@ container restart.
| Login | Passkey/WebAuthn (Fido2NetLib) |
| Database | SQLite via Entity Framework Core (default) - optionally PostgreSQL for horizontally scalable operation, see below |
| Deployment | Docker + Watchtower (default) - optionally Kubernetes/K3s for horizontal scaling, see below |
| CI/CD | GitLab CI with Semantic Release |
| CI/CD | GitHub Actions with Semantic Release |

---

## Deployment

### Prerequisites
- Docker and Docker Compose
- Access to the private registry `registry.example.com`

### Quick Start

Expand All @@ -182,21 +181,22 @@ chmod +x setup.sh

The setup script:
1. Creates `.env` from `.env.example`
2. Interactively asks for registry credentials
3. Logs Docker into the registry
4. Pulls the current image
5. Starts all services via `docker compose up -d`
2. Pulls the public `ghcr.io/lukislp/studylife-server` image (no login needed)
3. Starts all services via `docker compose up -d`

Running your own fork on your own registry instead? Set `SERVER_IMAGE` (and, if that registry is private, `REGISTRY_URL`/`REGISTRY_USER`/`REGISTRY_PASSWORD`) in `.env` - see `.env.example`.

On the very first start (no user registered yet), the server outputs a one-time setup code to the logs (`docker compose logs`) - this code is requested during the first passkey registration and protects against someone else on the same network claiming the initial registration before the actual operator. Every subsequent registration (e.g. family members) does not need this code.

### Environment Variables

| Variable | Default | Description |
|---|---|---|
| `REGISTRY_URL` | `registry.example.com` | Docker registry |
| `REGISTRY_USER` | - | Registry username |
| `REGISTRY_PASSWORD` | - | Registry password |
| `PORT` | `8080` | Public port |
| `SERVER_IMAGE` | `ghcr.io/lukislp/studylife-server:latest` | Server image to pull - override only if running your own fork on your own registry |
| `REGISTRY_URL` | - | Only needed with a private `SERVER_IMAGE`: registry to log into |
| `REGISTRY_USER` | - | Only needed with a private `SERVER_IMAGE`: registry username |
| `REGISTRY_PASSWORD` | - | Only needed with a private `SERVER_IMAGE`: registry password |

### Horizontally Scalable Operation (optional)

Expand All @@ -206,7 +206,7 @@ For more than a handful of users/higher load, the same server can also be run ag

## Automatic Updates

Watchtower is integrated in `docker-compose.yml` and checks every 5 minutes whether a new image is available. As soon as the GitLab pipeline publishes a new image, the container is restarted automatically.
Watchtower is integrated in `docker-compose.yml` and checks every 5 minutes whether a new image is available. As soon as the CI/CD pipeline publishes a new image, the container is restarted automatically.

Only containers with the label `com.centurylinklabs.watchtower.enable=true` are updated.

Expand Down Expand Up @@ -260,21 +260,24 @@ For non-interactive integrations like Home Assistant, which cannot maintain a pa

## CI/CD Pipeline

Runs as GitHub Actions (`.github/workflows/ci-cd.yml`) on every push to `main` and every pull request targeting it.

| Stage | Job | Description |
|---|---|---|
| test | secret_detection | GitLab's built-in secret-detection template |
| test | test:unit | Full `dotnet test` run (Shared + Server) |
| test | test:i18n | `check-i18n.py` - all 26 languages, every table |
| test | test:lint | `dotnet format --verify-no-changes` |
| test | test:security | NuGet vulnerability scan (non-blocking, fails visibly on High/Critical) |
| test | test:k8s-manifests | `kubeconform` schema validation of `k8s/` |
| test | test:compose-scale | Syntax/interpolation check of `docker-compose.scale.yml` |
| build | build | Restore and build all projects |
| version | get-version | Semantic Release dry run |
| publish | publish:server | dotnet publish + ZIP artifact |
| docker | docker:server | Multi-arch Docker image in registry |
| docker | trivy:server | Container vulnerability scan of the published image |
| release | semantic-release | GitLab release + changelog |
| test | `test-unit` | Full `dotnet test` run (Shared + Server), plus a self-hosted coverage badge (`.github/badges/coverage.json`) generated from the merged coverage report |
| test | `test-i18n` | `check-i18n.py` - all 26 languages, every table |
| test | `test-lint` | `dotnet format --verify-no-changes` |
| test | `test-security` | NuGet vulnerability scan (non-blocking, fails visibly on High/Critical) |
| test | `test-k8s-manifests` | `kubeconform` schema validation of `k8s/` |
| test | `test-compose-scale` | Syntax/interpolation check of `docker-compose.scale.yml` |
| build | `build` | Restore and build all projects (needs all test jobs to pass) |
| version | `get-version` | Semantic Release dry run against Conventional Commits; fails the run if no releasable version is determined. Push events only |
| publish | `publish-server` | `dotnet publish` (linux-x64 + linux-arm64) + ZIP artifact. Push to `main` only, and only if `get-version` found a releasable version |
| docker | `docker-server` | Multi-arch (amd64/arm64) Docker image, built and pushed to the public `ghcr.io/lukislp/studylife-server` registry |
| docker | `trivy-server` | Container vulnerability scan (Trivy) of the freshly published image, informational only (does not block the pipeline) |
| release | `semantic-release` | Real semantic-release run: publishes the GitHub release + changelog, commits the coverage badge |

`get-version` through `semantic-release` form a serialized release chain (`concurrency: studylife-release-chain`) and only run on pushes to `main`, never on pull requests.

Versioning via Conventional Commits:
- feat: minor version
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
services:

server:
image: registry.example.com/studylife/server:latest
image: ${SERVER_IMAGE:-ghcr.io/lukislp/studylife-server:latest}
container_name: studylife-server
restart: unless-stopped
labels:
Expand Down
44 changes: 15 additions & 29 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,21 @@ fi
tr -d '\r' < .env > .env.tmp && mv .env.tmp .env
set -a; source .env; set +a

# --- Ensure registry URL -----------------------------------------------
if [ -z "${REGISTRY_URL:-}" ]; then
read -rp "Registry URL (e.g. registry.example.com): " REGISTRY_URL
echo "REGISTRY_URL=${REGISTRY_URL}" >> .env
fi

# --- Prompt for registry credentials if not set -----------------------
if [ -z "${REGISTRY_USER:-}" ]; then
read -rp "Registry username: " REGISTRY_USER
echo "REGISTRY_USER=${REGISTRY_USER}" >> .env
fi
# --- Optional: only log in if a private registry override is configured ---
# The default image (ghcr.io/lukislp/studylife-server) is public - no login needed to
# pull it. This block only runs for people who set SERVER_IMAGE to their own fork's
# image on a private registry (see .env.example).
if [ -n "${REGISTRY_URL:-}" ] && [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
echo -e "${BOLD}[1/3] Logging into ${REGISTRY_URL}...${RESET}"
echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_URL}" -u "${REGISTRY_USER}" --password-stdin

if [ -z "${REGISTRY_PASSWORD:-}" ]; then
read -rsp "Registry password: " REGISTRY_PASSWORD
echo ""
echo "REGISTRY_PASSWORD=${REGISTRY_PASSWORD}" >> .env
fi

export REGISTRY_URL REGISTRY_USER REGISTRY_PASSWORD

# --- Registry login and pull images -----------------------------------------
echo ""
echo -e "${BOLD}[1/3] Logging into registry and pulling images...${RESET}"
echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_URL}" -u "${REGISTRY_USER}" --password-stdin

# Make Docker credentials available to Watchtower under /root/.docker/config.json
if [ ! -f /root/.docker/config.json ] && [ -f ~/.docker/config.json ]; then
mkdir -p /root/.docker
cp ~/.docker/config.json /root/.docker/config.json
# Make Docker credentials available to Watchtower under /root/.docker/config.json
if [ ! -f /root/.docker/config.json ] && [ -f ~/.docker/config.json ]; then
mkdir -p /root/.docker
cp ~/.docker/config.json /root/.docker/config.json
fi
else
echo -e "${BOLD}[1/3] Pulling public image (no registry login needed)...${RESET}"
fi

docker compose pull server
Expand All @@ -76,4 +62,4 @@ echo ""
echo " Logs : docker compose logs -f"
echo " Stop : docker compose down"
echo " Update : docker compose pull && docker compose up -d"
echo ""
echo ""