ci: use GitHub Container Registry

This commit is contained in:
2026-09-16 20:31:07 +02:00
parent f03e0e17de
commit b29b017341
10 changed files with 64 additions and 321 deletions
+19 -40
View File
@@ -1,10 +1,10 @@
# ANOUMA — Deployment
Production deployment is fully CI/CD-driven via Gitea or GitHub Actions. There is no `update.sh` and the production server never builds anything — it only ever pulls a tested image from the registry.
Production deployment is fully CI/CD-driven via GitHub Actions. There is no `update.sh` and the production server never builds anything — it only ever pulls a tested image from GitHub Container Registry.
```text
Developer → git push main → Gitea Actions (CI)
→ Tests → Docker Build → Registry Push (tagged by commit SHA)
Developer → git push main → GitHub Actions (CI)
→ Tests → Docker Build → GHCR Push (tagged by commit SHA)
→ Release (git tag vX.Y.Z) → promote that exact image to vX.Y.Z + latest
→ SSH → deploy.sh → docker compose pull → migrate → up -d → Healthcheck → LIVE
```
@@ -18,7 +18,7 @@ building from source and skipping the checks.
## One-time server setup
```bash
git clone https://git.maro.run/maro/anouma.git
git clone https://github.com/whitehai11/anouma.git
cd anouma
chmod +x setup.sh
./setup.sh
@@ -34,15 +34,15 @@ chmod +x setup.sh
6. Creates a restricted `anouma-deploy` system user, generates a dedicated SSH keypair for it, and installs the public key with a **forced command** so that key can only ever run `deploy.sh` — never an interactive shell.
7. Copies `docker-compose.yml` and `deploy.sh` into `/opt/anouma`.
8. Starts Postgres (so it's ready for the first deploy).
9. Prints the exact values to paste into **Gitea → maro/anouma → Settings → Secrets → Actions**, including the deploy private key — shown only once, the first time the key is generated.
9. Prints the exact values to paste into **GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions**, including the deploy private key — shown only once, the first time the key is generated.
Run `./setup.sh --non-interactive` for a scripted install; any value that has no safe default and wasn't exported as an environment variable beforehand aborts with a clear message naming the missing variable, instead of silently guessing.
Re-running `./setup.sh` later (e.g. after pulling script updates) is always safe: existing secrets, the SSH key, the database and all volumes are left exactly as they are — only `docker-compose.yml` and `deploy.sh` themselves get refreshed, since those are generated artifacts, not data.
### After setup: add the Gitea secrets
### After setup: add the GitHub secrets
Copy the block `setup.sh` printed into **Gitea → maro/anouma → Settings → Secrets → Actions**:
Copy the deployment values `setup.sh` printed into **GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions**:
| Secret | Value |
| --- | --- |
@@ -51,7 +51,7 @@ Copy the block `setup.sh` printed into **Gitea → maro/anouma → Settings →
| `DEPLOY_USER` | `anouma-deploy` |
| `DEPLOY_SSH_KEY` | the private key setup.sh printed |
| `DEPLOY_HOST_KEY` | optional — pin the server's SSH host public key instead of trusting it on first connect (`ssh-keyscan -p <port> <host>` on a machine you already trust) |
| `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` | registry push credentials (a Gitea access token as the password, not your account password) |
| `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` | **not needed in GitHub Actions**; these belong in `/opt/anouma/.env` on the server for pulling the private GHCR image |
### First deploy
@@ -60,28 +60,9 @@ Nothing runs on the server yet at this point — no release has been built. Push
```bash
git tag v1.0.0
git push github v1.0.0 # GitHub Actions
# alternativ: git push origin v1.0.0 # Gitea Actions
```
The selected CI provider takes it from there (see below). Once it's live, seed the original content once: `ssh anouma-deploy@<host>` won't work (the key is restricted — see Security), so run it directly on the server instead: `cd /opt/anouma && docker compose run --rm app npm run seed`.
## Gitea Actions
### CI (`.gitea/workflows/ci.yml`)
Runs on every push and PR: `npm ci` → lint → typecheck → test → build, on the `ubuntu-latest`-labelled runner. No database is required — every Payload-backed route in this app is `force-dynamic`, so a production build never touches Postgres. (There's no automated test suite yet — `npm test` is an honest placeholder; add a real one, e.g. vitest, whenever the project needs it, and it'll run here automatically.)
On top of that, every push to `main` that passes the checks above also builds the Docker image and pushes it to the registry tagged by commit SHA (`git.maro.run/maro/anouma:<sha>`) — this is the one and only build of that commit; nothing later rebuilds it.
### Release (`.gitea/workflows/release.yml`)
Triggered by pushing a tag matching `v*` (e.g. `v1.2.0`):
1. Pulls the image CI already built and pushed for that commit's SHA — fails immediately with a clear error if it isn't there (the tagged commit was never on `main`, or CI for it hasn't finished yet).
2. Retags that same image as `git.maro.run/maro/anouma:vX.Y.Z` **and** `:latest`, and pushes both.
3. SSHes into the production server as `anouma-deploy` and runs `deploy.sh deploy vX.Y.Z`.
Production always runs a specific version tag — `latest` is pushed for convenience/reference only (and always points at the most recently *released* build, not just the newest commit on `main`), `deploy.sh` never uses it.
The GitHub workflow takes it from there. Once it's live, seed the original content once: `ssh anouma-deploy@<host>` won't work (the key is restricted — see Security), so run it directly on the server instead: `cd /opt/anouma && docker compose run --rm app npm run seed`.
## GitHub Actions
@@ -91,28 +72,26 @@ The GitHub repository is `whitehai11/anouma`; the workflows live in
| Secret | Required | Value |
| --- | --- | --- |
| `REGISTRY_USERNAME` | yes | Gitea Container-Registry username, normally `maro` |
| `REGISTRY_PASSWORD` | yes | Gitea access token/password with permission to push/pull `git.maro.run/maro/anouma` |
| `DEPLOY_HOST` | yes | Public IP or hostname of the production server |
| `DEPLOY_PORT` | yes | SSH port, normally `22` |
| `DEPLOY_USER` | yes | `anouma-deploy` |
| `DEPLOY_SSH_KEY` | yes | Private key generated by `setup.sh` for the restricted deploy user |
| `DEPLOY_HOST_KEY` | no | Complete SSH host-key line from a trusted `ssh-keyscan`; safer than first-connect trust |
Es werden keine zusätzlichen GitHub-Variables benötigt; Registry und Image
sind im Workflow fest auf `git.maro.run/maro/anouma` gesetzt. Runtime-Secrets
GitHub Actions authentifiziert sich mit dem automatisch bereitgestellten
`GITHUB_TOKEN`; dafür musst du keine Registry-Secrets anlegen. Registry und
Image sind im Workflow fest auf `ghcr.io/whitehai11/anouma` gesetzt. Runtime-Secrets
wie `PAYLOAD_SECRET`, `MEETING_SESSION_SECRET`, `LIVEKIT_API_SECRET` und
`DATABASE_URI` gehören ausschließlich in `/opt/anouma/.env` auf dem Server und
nicht in GitHub Actions.
Wichtig: Für Releases nur einen CI-Anbieter verwenden, damit nicht beide
Systeme dasselbe Image parallel bauen oder deployen. Für dieses GitHub-Setup
ist GitHub Actions die empfohlene Quelle; Gitea kann als Spiegel bestehen
bleiben.
Wichtig: Für Releases nur GitHub Actions verwenden. Gitea kann als reiner
Repository-Spiegel bestehen bleiben, darf aber keine eigenen Build- oder
Release-Workflows ausführen.
## `deploy.sh` (server-side, `/opt/anouma/deploy.sh`)
Not a manual tool — it's invoked exclusively by Gitea Actions over SSH. What it does for `deploy.sh deploy vX.Y.Z`:
Not a manual tool — it's invoked exclusively by GitHub Actions over SSH. What it does for `deploy.sh deploy vX.Y.Z`:
```text
Lock (refuse a second concurrent deploy)
@@ -126,7 +105,7 @@ Lock (refuse a second concurrent deploy)
→ success, or: automatic rollback to the previous version
```
If the post-deploy healthcheck fails, `deploy.sh` automatically redeploys the **previous** version (recorded from `ANOUMA_IMAGE` before the attempt) and reports the failed deploy as exit code 1 — Gitea Actions shows the release as failed even though production recovered. Database migrations from the failed release are **not** reverted (write migrations to be forward-compatible); restore the pre-deploy backup manually if a migration truly needs undoing.
If the post-deploy healthcheck fails, `deploy.sh` automatically redeploys the **previous** version (recorded from `ANOUMA_IMAGE` before the attempt) and reports the failed deploy as exit code 1 — GitHub Actions shows the release as failed even though production recovered. Database migrations from the failed release are **not** reverted (write migrations to be forward-compatible); restore the pre-deploy backup manually if a migration truly needs undoing.
A lock file (`/opt/anouma/.deploy.lock`) prevents two deploys from running at once; a stale lock from a killed process is detected and ignored automatically.
@@ -242,7 +221,7 @@ Neither `setup.sh` nor `deploy.sh` ever runs `docker system prune`, `docker volu
- `anouma-deploy` **is** a member of the `docker` group, because running `docker compose` requires it — and Docker-group membership is well known to be effectively root-equivalent (a container can bind-mount the host filesystem). This is a deliberate, documented trade-off: the blast radius is contained by the two points above (no interactive access, and the only reachable code path is the fixed `deploy.sh` logic), not eliminated. For a stricter setup, consider rootless Docker or a dedicated deployment tool with finer-grained Docker API ACLs — out of scope here.
- `.env` and the deploy SSH private key are `chmod 600`.
- Secrets are never committed, never logged, and never printed more than once (`setup.sh` shows the private key only the first time it's generated; on a later run it prints "Existing deployment key found." instead).
- `REGISTRY_PASSWORD`/`SMTP_PASSWORD`/etc. are masked (`********`) in `setup.sh`'s configuration summary — only the one-time Gitea-secrets block shows real values, since that block exists specifically for you to copy them into Gitea.
- `REGISTRY_PASSWORD`/`SMTP_PASSWORD`/etc. are masked (`********`) in `setup.sh`'s configuration summary. The GHCR token is needed only on the production server for pulling the private image; GitHub Actions uses its automatic `GITHUB_TOKEN`.
## Troubleshooting
@@ -250,7 +229,7 @@ Neither `setup.sh` nor `deploy.sh` ever runs `docker system prune`, `docker volu
| --- | --- |
| `setup.sh` fails at "Datenbank wurde nicht rechtzeitig healthy" | `docker compose logs postgres` (from `/opt/anouma`) — usually a bad `POSTGRES_PASSWORD`/`DATABASE_URI` mismatch if you hand-edited `.env` |
| Release workflow fails at "Pull the tested image" | The tagged commit was never pushed to `main`, or `main`'s CI/build-image run for it hasn't finished yet — push to `main`, wait for CI to go green, then tag |
| Release workflow fails at "Deploy to production" | Check the Gitea secrets are all set correctly, and that the server's SSH port is reachable from the runner |
| Release workflow fails at "Deploy to production" | Check the GitHub repository secrets are all set correctly, and that the server's SSH port is reachable from the runner |
| App container unhealthy after a deploy | `ssh` directly to the server (not via the restricted key) and run `docker compose logs app` from `/opt/anouma` |
| Video calls connect but no audio/video | Check LiveKit logs, the UDP 3478/50000–60000 firewall rules and the public `LIVEKIT_URL` |
| Browser cannot connect to LiveKit | Check the LiveKit proxy host, trusted TLS certificate and that Websockets Support is enabled |