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
+7 -7
View File
@@ -80,19 +80,19 @@ NPM_NETWORK=
BACKUP_RETENTION_DAYS=14
# ── Container registry & deployment (used by deploy.sh) ────────────────
# Gitea's built-in container registry. deploy.sh builds nothing — it only
# ever pulls this image (see .gitea/workflows/release.yml, which is what
# actually builds and pushes it).
REGISTRY=git.maro.run
REGISTRY_REPO=maro/anouma
# GitHub Container Registry. deploy.sh builds nothing — it only ever pulls
# the image that GitHub Actions built and pushed.
REGISTRY=ghcr.io
REGISTRY_REPO=whitehai11/anouma
# Only needed if the registry/repository requires authentication to pull.
# Use a Gitea access token as the password, not your account password.
# Use a GitHub token with at least read:packages as the password, not your
# account password.
REGISTRY_USERNAME=
REGISTRY_PASSWORD=
# Set automatically by deploy.sh on every successful deploy — do not edit
# by hand. Empty until the first release has been deployed.
ANOUMA_IMAGE=
# Informational only (shown in the setup summary) — actual automatic
# deployment is controlled by whether release.yml + its Gitea secrets are
# deployment is controlled by whether release.yml + its GitHub secrets are
# configured, not by this flag.
AUTO_DEPLOY=true
-118
View File
@@ -1,118 +0,0 @@
name: CI
on:
push:
branches: ['**']
pull_request:
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:22-alpine
# No live database is needed here — every Payload-backed route in this
# app is force-dynamic, so `next build` never touches Postgres. These
# are dummy values only so a real config error (a missing secret) is
# never silently masked by CI happening to run without one.
#
# NODE_ENV is deliberately NOT set here: `npm ci` treats a production
# NODE_ENV as `--omit=dev`, which would skip eslint/typescript/etc. and
# break Lint/Typecheck/Test. It's set below, scoped to the Build step.
env:
NEXT_TELEMETRY_DISABLED: "1"
DATABASE_URI: postgresql://ci:ci@localhost:5432/ci
PAYLOAD_SECRET: ci-only-not-a-real-secret
MEETING_SESSION_SECRET: ci-only-not-a-real-secret
ANALYTICS_SALT_SECRET: ci-only-not-a-real-secret
CRON_SECRET: ci-only-not-a-real-secret
NEXT_PUBLIC_SERVER_URL: http://localhost:3000
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies (sharp needs libc6-compat on alpine)
run: apk add --no-cache libc6-compat
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npx next typegen && npx tsc --noEmit
- name: Test
run: npm test
- name: Build
env:
NODE_ENV: production
run: npm run build
# Builds once per commit that lands on main and has passed the checks
# above, and pushes it to the registry tagged by commit SHA. release.yml
# (triggered by a version tag) never rebuilds — it only promotes this
# exact, already-tested image to the release tag + `latest` and deploys
# it. This guarantees byte-for-byte that what gets deployed is what CI
# verified, and makes tagging a commit that never passed CI on main fail
# loudly (no image to promote) instead of silently rebuilding from
# source and skipping the checks.
build-image:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
# The anouma-runner runs as a Docker container itself (gitea/runner:3),
# with the host's docker.sock bind-mounted in but no `docker` CLI binary
# of its own — a job with no `container:` here runs directly inside that
# minimal runner image and gets "docker: command not found".
#
# A plain `docker:27-cli` image (Alpine, no Node.js) doesn't work either:
# actions/checkout@v4 is a JS action and needs a Node runtime in the job
# container, which that image doesn't have — it fails at Checkout with
# exit 127. Using a Node image as the base guarantees Node is already
# there (same reason the `test` job's node:22-alpine works), and the
# Docker CLI is added via apt. The runner auto-mounts the same host
# docker.sock into this container (sibling containers — see
# container.docker_host in the runner's config.yaml), so `docker
# build`/`push` operate on the host's real Docker daemon. release.yml's
# promote-and-deploy job uses the same setup, for the same reasons.
container:
image: node:22-bookworm-slim
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker CLI
run: |
apt-get update
apt-get install -y --no-install-recommends docker.io
rm -rf /var/lib/apt/lists/*
- name: Determine image name
id: image
run: |
echo "registry=${REGISTRY:-git.maro.run}" >> "$GITHUB_OUTPUT"
echo "repo=${REGISTRY_REPO:-maro/anouma}" >> "$GITHUB_OUTPUT"
env:
REGISTRY: git.maro.run
REGISTRY_REPO: maro/anouma
- name: Log in to registry
env:
REGISTRY_IMAGE: ${{ steps.image.outputs.registry }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_IMAGE" -u "$REGISTRY_USERNAME" --password-stdin
- name: Build image
env:
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
# Tagged by commit SHA only — `latest` is reserved for whatever was
# last actually released (see release.yml), not the newest main build.
run: docker build -t "$IMAGE:$GITHUB_SHA" .
- name: Push image
env:
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
run: docker push "$IMAGE:$GITHUB_SHA"
-120
View File
@@ -1,120 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
# Deliberately does NOT rebuild or re-run lint/typecheck/test/build: the
# image for this exact commit was already built and pushed by ci.yml's
# build-image job when it landed on main (which itself only runs after
# the `test` job passes). This job just promotes that already-verified
# image to the release tag + `latest` and deploys it — if the tagged
# commit was never pushed to main (or CI hasn't finished yet), the
# "Pull the tested image" step below fails loudly instead of silently
# rebuilding from source and skipping the checks.
promote-and-deploy:
runs-on: ubuntu-latest
# Node-based image (not the bare runner, not a Docker-only image) for
# the same reasons as ci.yml's build-image job: the runner needs a
# Docker CLI added via apt (it has none of its own), and actions/checkout
# needs a Node runtime that a plain docker:*-cli image doesn't have.
container:
image: node:22-bookworm-slim
steps:
- name: Install Docker CLI and ssh client
run: |
apt-get update
apt-get install -y --no-install-recommends docker.io openssh-client
rm -rf /var/lib/apt/lists/*
- name: Determine image tags
id: image
# Gitea Actions' runner is protocol-compatible with GitHub Actions,
# so step commands use the same $GITHUB_OUTPUT / $GITHUB_ENV
# mechanism (the `gitea.*` vs `github.*` expression contexts are
# just aliases of each other for `${{ }}` templating).
#
# The tag name (github.ref_name / $GITHUB_REF_NAME) is
# attacker-influenced: anyone with tag-push rights controls it, and
# git allows shell metacharacters in tag names. It's read here via
# the automatic $GITHUB_REF_NAME env var rather than `${{ github.ref_name }}`
# so its value reaches bash as data (argv/envp), never spliced into
# the script text — `${{ }}` expressions are substituted into the
# YAML before bash ever sees the script, which is what makes
# interpolating attacker-controlled values that way a script-injection
# vector (arbitrary code execution in a job that holds the registry
# and production deploy credentials).
run: |
echo "registry=${REGISTRY:-git.maro.run}" >> "$GITHUB_OUTPUT"
echo "repo=${REGISTRY_REPO:-maro/anouma}" >> "$GITHUB_OUTPUT"
echo "version=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
env:
REGISTRY: git.maro.run
REGISTRY_REPO: maro/anouma
- name: Log in to registry
env:
REGISTRY_IMAGE: ${{ steps.image.outputs.registry }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_IMAGE" -u "$REGISTRY_USERNAME" --password-stdin
- name: Pull the tested image for this commit
env:
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
run: |
docker pull "$IMAGE:$GITHUB_SHA" || {
echo "::error::No image found for commit $GITHUB_SHA. This tag must point at a commit that was already pushed to main and built by ci.yml's build-image job — push to main first (and wait for CI to finish) before tagging a release." >&2
exit 1
}
- name: Promote image to the release tag and latest
env:
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
VERSION: ${{ steps.image.outputs.version }}
run: |
docker tag "$IMAGE:$GITHUB_SHA" "$IMAGE:$VERSION"
docker tag "$IMAGE:$GITHUB_SHA" "$IMAGE:latest"
- name: Push promoted tags
env:
IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }}
VERSION: ${{ steps.image.outputs.version }}
run: |
docker push "$IMAGE:$VERSION"
docker push "$IMAGE:latest"
- name: Deploy to production
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
# Optional: pin the exact host key instead of trusting it on
# first connect (see DEPLOYMENT.md). Safe to leave unset.
DEPLOY_HOST_KEY: ${{ secrets.DEPLOY_HOST_KEY }}
VERSION: ${{ steps.image.outputs.version }}
shell: bash
run: |
set -euo pipefail
umask 077
key_file="$(mktemp)"
known_hosts_file="$(mktemp)"
trap 'rm -f "$key_file" "$known_hosts_file"' EXIT
printf '%s\n' "$DEPLOY_SSH_KEY" > "$key_file"
chmod 600 "$key_file"
if [ -n "${DEPLOY_HOST_KEY:-}" ]; then
printf '%s\n' "$DEPLOY_HOST_KEY" > "$known_hosts_file"
else
ssh-keyscan -p "${DEPLOY_PORT:-22}" "$DEPLOY_HOST" > "$known_hosts_file" 2>/dev/null
fi
# The forced command in the server's authorized_keys (see
# setup.sh) ignores this and always runs deploy.sh itself — the
# string below only ever reaches it via $SSH_ORIGINAL_COMMAND.
ssh -i "$key_file" -o UserKnownHostsFile="$known_hosts_file" -p "${DEPLOY_PORT:-22}" \
"${DEPLOY_USER}@${DEPLOY_HOST}" "deploy ${VERSION}"
+5 -4
View File
@@ -46,17 +46,18 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE: git.maro.run/maro/anouma
IMAGE: ghcr.io/whitehai11/anouma
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: git.maro.run
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image
run: docker build -t "$IMAGE:$GITHUB_SHA" .
- name: Push image
+5 -4
View File
@@ -6,19 +6,20 @@ on:
permissions:
contents: read
packages: write
jobs:
promote-and-deploy:
runs-on: ubuntu-latest
env:
IMAGE: git.maro.run/maro/anouma
IMAGE: ghcr.io/whitehai11/anouma
steps:
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: git.maro.run
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull tested image
run: docker pull "$IMAGE:$GITHUB_SHA"
- name: Promote release image
+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 |
+5 -5
View File
@@ -40,16 +40,16 @@ Der Versand ist idempotent (`reminder60Sent`/`reminder30Sent` je Anmeldung, `hos
## Setup (Produktion / Deployment)
Produktion läuft komplett über Gitea Actions CI/CD — der Server baut nichts selbst, er zieht ein fertiges, getestetes Image aus der Registry. Einmalig auf dem Server:
Produktion läuft komplett über GitHub Actions CI/CD — der Server baut nichts selbst, er zieht ein fertiges, getestetes Image aus GitHub Container Registry. Einmalig auf dem Server:
```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
```
`setup.sh` installiert bei Bedarf Docker, legt `/opt/anouma` an, generiert alle Secrets automatisch (nie erneut, wenn schon vorhanden), richtet einen eingeschränkten Deployment-User samt SSH-Key für Gitea Actions ein und zeigt am Ende genau die Werte an, die als Gitea Secrets hinterlegt werden müssen. `./setup.sh --non-interactive` läuft ohne Rückfragen.
`setup.sh` installiert bei Bedarf Docker, legt `/opt/anouma` an, generiert alle Secrets automatisch (nie erneut, wenn schon vorhanden), richtet einen eingeschränkten Deployment-User samt SSH-Key für GitHub Actions ein und zeigt am Ende genau die Werte an, die als GitHub Repository Secrets hinterlegt werden müssen. Das Docker-Image landet unter `ghcr.io/whitehai11/anouma`. `./setup.sh --non-interactive` läuft ohne Rückfragen.
Jedes weitere Deployment läuft danach ausschließlich über einen Git-Tag:
@@ -57,9 +57,9 @@ Jedes weitere Deployment läuft danach ausschließlich über einen Git-Tag:
git tag v1.2.0 && git push origin v1.2.0
```
Gitea Actions testet, baut das Image, pusht es in die Registry und deployt es per SSH auf den Server — inklusive Backup, Healthcheck und automatischem Rollback bei Fehlschlag. Es gibt kein separates Update-Script.
GitHub Actions testet, baut das Image, pusht es nach GHCR und deployt es per SSH auf den Server — inklusive Backup, Healthcheck und automatischem Rollback bei Fehlschlag. Es gibt kein separates Update-Script.
Für alle Details (Architektur, Gitea Secrets, CI/Release-Workflows, Reverse-Proxy- und LiveKit-Einrichtung, Environment-Variablen, Backups, Rollback, Security) siehe **[DEPLOYMENT.md](./DEPLOYMENT.md)**.
Für alle Details (Architektur, GitHub Secrets, CI/Release-Workflows, Reverse-Proxy- und LiveKit-Einrichtung, Environment-Variablen, Backups, Rollback, Security) siehe **[DEPLOYMENT.md](./DEPLOYMENT.md)**.
## Lokale Entwicklung (ohne Docker für die App)
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# ANOUMA — server-side deployment script.
#
# This is NOT a manual update tool. It is invoked exclusively by Gitea
# This is NOT a manual update tool. It is invoked exclusively by GitHub
# Actions over SSH as the restricted `anouma-deploy` user (see setup.sh,
# which installs a forced-command SSH key that can only ever run this
# script). The production server never builds an image — it only ever
@@ -106,7 +106,7 @@ backup_database() {
registry_login() {
if [ -n "${REGISTRY_USERNAME:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
echo "$REGISTRY_PASSWORD" | docker login "${REGISTRY:-git.maro.run}" -u "$REGISTRY_USERNAME" --password-stdin >/dev/null 2>&1 \
echo "$REGISTRY_PASSWORD" | docker login "${REGISTRY:-ghcr.io}" -u "$REGISTRY_USERNAME" --password-stdin >/dev/null 2>&1 \
&& log_success "Registry login OK." \
|| fail "Registry login failed — check REGISTRY_USERNAME/REGISTRY_PASSWORD in .env."
fi
@@ -116,7 +116,7 @@ registry_login() {
# the rest of this script (and `docker compose`, via env_file) picks it up.
set_image_version() {
local version="$1"
env_set ANOUMA_IMAGE "${REGISTRY:-git.maro.run}/${REGISTRY_REPO:-maro/anouma}:${version}"
env_set ANOUMA_IMAGE "${REGISTRY:-ghcr.io}/${REGISTRY_REPO:-whitehai11/anouma}:${version}"
set -a; source .env; set +a
}
+2 -2
View File
@@ -30,10 +30,10 @@ services:
app:
# Production never builds from source — the image is built once by
# Gitea Actions on release and pushed to the registry; this host only
# GitHub Actions on release and pushed to GHCR; this host only
# ever pulls a specific, already-tested tag (see deploy.sh, which is the
# only thing that ever changes ANOUMA_IMAGE in .env).
image: ${ANOUMA_IMAGE:-git.maro.run/maro/anouma:latest}
image: ${ANOUMA_IMAGE:-ghcr.io/whitehai11/anouma:latest}
restart: unless-stopped
networks:
- anouma-network
+18 -18
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env bash
# ANOUMA — production server installer.
#
# git clone https://git.maro.run/maro/anouma.git && cd anouma
# git clone https://github.com/whitehai11/anouma.git && cd anouma
# chmod +x setup.sh && ./setup.sh
#
# Prepares a fresh Ubuntu/Debian server for ANOUMA: installs Docker if
# needed, creates /opt/anouma, writes a production .env with generated
# secrets, creates a restricted `anouma-deploy` SSH user for Gitea Actions,
# secrets, creates a restricted `anouma-deploy` SSH user for GitHub Actions,
# and installs the server-side deploy.sh + docker-compose.yml.
#
# From then on, Gitea Actions owns all deployments (see
# .gitea/workflows/release.yml) — there is no update.sh. Pushing a release
# From then on, GitHub Actions owns all deployments (see
# .github/workflows/release.yml) — there is no update.sh. Pushing a release
# tag (vX.Y.Z) is the only way production gets updated.
#
# Safe to re-run: never regenerates an existing secret or SSH key, never
@@ -151,7 +151,7 @@ command -v wget >/dev/null 2>&1 || log_info "wget nicht gefunden — wird hier n
if systemctl is-active --quiet ssh 2>/dev/null || systemctl is-active --quiet sshd 2>/dev/null; then
log_success "SSH-Server läuft."
else
log_warn "Kein laufender SSH-Server erkannt (ssh/sshd) — Gitea Actions kann ohne einen laufenden SSH-Server nicht deployen. Installiere/starte z. B. 'openssh-server'."
log_warn "Kein laufender SSH-Server erkannt (ssh/sshd) — GitHub Actions kann ohne einen laufenden SSH-Server nicht deployen. Installiere/starte z. B. 'openssh-server'."
fi
# ---------------------------------------------------------------------------
@@ -317,8 +317,8 @@ GOOGLE_SITE_VERIFICATION=
DOCKER_NETWORK=anouma-network
NPM_NETWORK=
BACKUP_RETENTION_DAYS=14
REGISTRY=git.maro.run
REGISTRY_REPO=maro/anouma
REGISTRY=ghcr.io
REGISTRY_REPO=whitehai11/anouma
REGISTRY_USERNAME=
REGISTRY_PASSWORD=
ANOUMA_IMAGE=
@@ -380,7 +380,7 @@ if [ -z "$(env_get NPM_NETWORK)" ] && [ "$NPM_DETECTED" = true ]; then
fi
# --- registry --------------------------------------------------------------
REGISTRY_HOST="$(env_get REGISTRY)"; REGISTRY_HOST="${REGISTRY_HOST:-git.maro.run}"
REGISTRY_HOST="$(env_get REGISTRY)"; REGISTRY_HOST="${REGISTRY_HOST:-ghcr.io}"
log_info "Prüfe Registry-Erreichbarkeit ($REGISTRY_HOST) …"
REGISTRY_REACHABLE=false
REGISTRY_HTTP_CODE="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 5 "https://${REGISTRY_HOST}/v2/" 2>/dev/null || echo "000")"
@@ -405,7 +405,7 @@ if [ -z "$(env_get REGISTRY_USERNAME)" ]; then
NEED_REGISTRY_AUTH="$(ask_yn "Werden Zugangsdaten für die Container Registry benötigt (privates Repository)?" "Y")"
if [ "$NEED_REGISTRY_AUTH" = true ]; then
REG_USER="$(ask "Registry Username" "" "REGISTRY_USERNAME")"
REG_PASS="$(ask_secret "Registry Password/Token (Gitea Personal Access Token empfohlen)" "REGISTRY_PASSWORD")"
REG_PASS="$(ask_secret "Registry Password/Token (GitHub Token mit read:packages)" "REGISTRY_PASSWORD")"
if [ -z "$REG_USER" ] || [ -z "$REG_PASS" ]; then
if [ "$NON_INTERACTIVE" = true ]; then
fail "Registry-Zugangsdaten fehlen." "Setze die Umgebungsvariablen REGISTRY_USERNAME und REGISTRY_PASSWORD und starte setup.sh erneut, oder führe es interaktiv aus."
@@ -421,7 +421,7 @@ else
fi
if [ -z "$(env_get AUTO_DEPLOY)" ] || [ "$FRESH_INSTALL" = true ]; then
AUTO_DEPLOY_ANSWER="$(ask_yn "Enable automatic deployment (Gitea Actions deployt automatisch bei neuen Releases)?" "Y")"
AUTO_DEPLOY_ANSWER="$(ask_yn "Enable automatic deployment (GitHub Actions deployt automatisch bei neuen Releases)?" "Y")"
env_set AUTO_DEPLOY "$AUTO_DEPLOY_ANSWER"
fi
@@ -590,7 +590,7 @@ if [ -n "$EXISTING_IMAGE" ]; then
log_warn "Anwendung konnte nicht (neu) gestartet werden — prüfe: $COMPOSE logs app"
fi
else
log_info "Noch kein Release deployt — die Anwendung wird beim ersten Git-Tag automatisch über Gitea Actions gestartet."
log_info "Noch kein Release deployt — die Anwendung wird beim ersten Git-Tag automatisch über GitHub Actions gestartet."
if [ "$SKIP_SEED" = false ]; then
log_info "Hinweis: Inhalte können nach dem ersten Deploy mit 'docker compose run --rm app npm run seed' eingespielt werden."
fi
@@ -635,7 +635,7 @@ echo ""
echo "Registry:"
if [ "$REGISTRY_REACHABLE" = true ]; then echo " ✓ ($(env_get REGISTRY))"; else echo " ⚠ nicht erreichbar geprüft ($(env_get REGISTRY))"; fi
echo ""
echo "Gitea Actions:"
echo "GitHub Actions:"
echo " Ready"
echo ""
echo "Automatic Deployment:"
@@ -643,11 +643,11 @@ if [ "$(env_get AUTO_DEPLOY)" = "true" ]; then echo " Enabled"; else echo " Di
echo ""
echo "========================================"
echo "GITEA SECRETS"
echo "GITHUB DEPLOY SECRETS"
echo "========================================"
echo ""
if [ "$SSH_KEY_FRESH" = true ]; then
echo "Trage folgende Werte EINMALIG unter Gitea → maro/anouma → Settings → Secrets → Actions ein."
echo "Trage folgende Werte EINMALIG unter GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions ein."
echo "Der private Key wird nach diesem Lauf nicht erneut angezeigt."
echo ""
echo "DEPLOY_HOST:"
@@ -664,13 +664,13 @@ if [ "$SSH_KEY_FRESH" = true ]; then
$SUDO cat "$KEY_PATH"
echo "----------------------------------------"
echo ""
echo "REGISTRY_USERNAME:"
echo "GHCR SERVER USERNAME (not a GitHub Secret):"
echo " $(env_get REGISTRY_USERNAME)"
echo ""
echo "REGISTRY_PASSWORD:"
echo "GHCR SERVER TOKEN (not a GitHub Secret):"
echo " $(env_get REGISTRY_PASSWORD)"
else
echo "Existing deployment key found — Gitea Secrets wurden bereits bei der ersten Einrichtung ausgegeben."
echo "Existing deployment key found — GitHub Secrets wurden bereits bei der ersten Einrichtung ausgegeben."
echo "Falls sie nicht mehr vorliegen: DEPLOY_SSH_KEY kann sicher eingesehen werden mit:"
echo " sudo cat $KEY_PATH"
fi
@@ -681,7 +681,7 @@ echo "NEXT STEP"
echo "========================================"
echo ""
echo "Add these values to:"
echo " Gitea → maro/anouma → Settings → Secrets → Actions"
echo " GitHub → whitehai11/anouma → Settings → Secrets and variables → Actions"
echo ""
echo "Danach reicht für jedes zukünftige Deployment:"
echo " git tag v1.0.0 && git push origin v1.0.0"