# ANOUMA — Deployment Full Docker-based deployment guide. For local development without Docker (running `next dev` directly against a Dockerized Postgres), see `README.md` instead. ## Quick start ```bash git clone https://git.maro.run/maro/anouma.git cd anouma ./setup.sh ``` That's it — `setup.sh` builds the images, starts Postgres, waits for it to be healthy, runs migrations, optionally seeds the original ANOUMA content, starts the app, and prints a status summary. Docker and Docker Compose are the only host requirements; Node.js/npm are **not** needed on the host — everything runs inside the `app` container. Run `./setup.sh --non-interactive` for sensible defaults with no prompts (useful for scripted/CI installs), or `--skip-seed` to skip content seeding. `setup.sh` is safe to re-run on an existing installation — it never overwrites a secret that's already in `.env`, never deletes a volume, network or container, and never touches an existing Nginx Proxy Manager (or any other container) beyond optionally joining its network. ## Architecture ``` docker-compose.yml │ ├── postgres PostgreSQL 16, persisted in the "pgdata" volume │ └── app Next.js + Payload CMS + WebRTC signaling (server.ts) — one process, one container. There is no separate "signaling" service: the WebSocket signaling server is attached to the same custom Node server that serves the website and admin panel (see server.ts), so it scales and deploys as a single unit. ``` Both containers join the internal `anouma-network` (name configurable via `DOCKER_NETWORK` in `.env`). The `app` container optionally also joins an external reverse-proxy network — see below. ## Nginx Proxy Manager (or any reverse proxy) `setup.sh` looks for a running Nginx Proxy Manager container (or a Docker network whose name looks like one) and, if found, asks whether to join its network. If you'd rather do this manually (or NPM wasn't running yet when you set up ANOUMA): 1. Find its network: `docker network ls` 2. Set `NPM_NETWORK=` in `.env` 3. `cp docker-compose.override.yml.example docker-compose.override.yml` 4. `docker compose up -d` — Compose picks up `docker-compose.override.yml` automatically `docker-compose.override.yml` is machine-specific and gitignored on purpose. In Nginx Proxy Manager, add a Proxy Host: | Field | Value | | --- | --- | | Domain | your domain, e.g. `anouma.org` | | Scheme | `http` | | Forward Host | `app` (the Compose service name — reachable by name once on the same network) | | Forward Port | `3000` | | **Websockets Support** | **enabled** — required for the video-call signaling (`/ws/signaling`) | Once a reverse proxy reaches the container directly over the shared network, you can remove the `ports:` mapping for `app` in `docker-compose.yml` so the app isn't also reachable directly on the host. If no reverse proxy is configured, ANOUMA still works standalone — the app is published on `http://localhost:${APP_PORT:-3000}` (bound to `127.0.0.1` by default; open that up in `docker-compose.yml` if you need it reachable from outside without a proxy). ## Environment variables See `.env.example` for the full list with inline explanations. Only variables the application actually reads are defined — highlights: | Variable | Purpose | | --- | --- | | `DATABASE_URI`, `POSTGRES_*` | Postgres connection — kept in sync automatically by `setup.sh` | | `PAYLOAD_SECRET`, `MEETING_SESSION_SECRET`, `CRON_SECRET` | Auto-generated on first run, never overwritten afterwards | | `NEXT_PUBLIC_SERVER_URL` | Public URL of the site (used for admin/media links and email links) | | `SMTP_*` | The existing ANOUMA mail system (booking/reminder emails) | | `STUN_SERVER`, `TURN_SERVER`, `TURN_USERNAME`, `TURN_PASSWORD` | WebRTC ICE servers — see below | | `DOCKER_NETWORK`, `NPM_NETWORK` | Docker network names (see above) | | `AUTO_UPDATE`, `BACKUP_RETENTION_DAYS` | See Updates below | Secrets are only ever written to `.env` on your server, never committed (`.gitignore` excludes `.env*` except `.env.example`). ## WebRTC: signaling, STUN, TURN ``` Client A Client B │ │ │ WebSocket (wss://…/ws/signaling) │ ▼ ▼ Signaling server (in the "app" container) │ ▼ relays only small JSON offer/answer/ICE messages — never touches audio/video/screen Client A ═══════════ P2P WebRTC ═══════════ Client B Audio / Video / Screen ``` The signaling server never sees media. By default, clients use Google's public STUN server (`STUN_SERVER` in `.env`), which is enough for most networks. Behind strict NATs/corporate firewalls, P2P via STUN alone can fail — add a TURN server by setting `TURN_SERVER`, `TURN_USERNAME`, `TURN_PASSWORD` in `.env` and restarting the app container; no code changes are required. (This project doesn't run a TURN server itself — coturn is a common self-hosted option if you need one.) ## Updates ```bash ./update.sh # update to the latest release tag ./update.sh v1.2.0 # update to a specific tag ``` Update flow: backup → fetch tags → checkout the release → rebuild → migrate → restart → healthcheck. If the healthcheck fails, the **code** is automatically rolled back to the previous version (database migrations are not reverted — write migrations to be forward-compatible; see Rollback below). Only real release tags (`vX.Y.Z`) are deployed — `update.sh` deliberately never force-deploys whatever happens to be on `main`. If no tags exist yet in the repository, it does nothing and says so. An update lock (`.update.lock`) prevents two updates from running concurrently. ### Automatic updates Off by default (`AUTO_UPDATE=false`). Set `AUTO_UPDATE=true` in `.env` and re-run `./setup.sh` to install a cron job that checks for new releases every 30 minutes and updates automatically when one appears — otherwise it's a no-op. Disable again by setting `AUTO_UPDATE=false` and removing the `# anouma-auto-update` line from `crontab -e`. ### Backups Every update creates `backups/database-YYYY-MM-DD-HHMM.sql.gz` before touching anything. Backups older than `BACKUP_RETENTION_DAYS` (default 14) are cleaned up automatically — the backup just created is never deleted, even if retention is set very low. Uploaded media lives in the `./media` bind mount, which isn't touched by updates at all. ### Rollback Automatic on a failed post-update healthcheck (see above). To roll back manually: ```bash git checkout vX.Y.Z docker compose build docker compose up -d ``` If a migration from the failed release isn't backward-compatible, restore the pre-update backup instead: ```bash gunzip -c backups/database-2026-08-25-1430.sql.gz | docker compose exec -T postgres psql -U postgres anouma ``` ## Persistent data Nothing about rebuilding or updating containers ever deletes data: - **Database** — the `pgdata` named volume, independent of the `postgres` container's lifecycle. - **Uploaded media** — the `./media` bind mount, independent of the `app` container's lifecycle. - **Backups** — the `./backups` directory on the host. `setup.sh` and `update.sh` never run `docker system prune`, `docker volume prune`, `docker network prune`, or `docker compose down -v` — none of the scripts in this repo do. ## Healthchecks - **app**: `GET /api/health` (built into the Docker image's `HEALTHCHECK`) — checks that the app can actually query Postgres, not just that the process is listening. - **postgres**: `pg_isready`. `docker compose ps` shows both statuses. ## Troubleshooting | Symptom | Check | | --- | --- | | `setup.sh` fails at "Datenbank wurde nicht rechtzeitig healthy" | `docker compose logs postgres` — usually a bad `POSTGRES_PASSWORD`/`DATABASE_URI` mismatch if you hand-edited `.env` | | App container unhealthy | `docker compose logs app`, then `curl http://localhost:3000/api/health` from inside the network (`docker compose exec app wget -qO- http://127.0.0.1:3000/api/health`) | | Video calls connect but no audio/video | Likely a restrictive NAT — configure a TURN server (see above) | | Reverse proxy shows a 502/connection reset on the call page | Websockets Support isn't enabled on the Nginx Proxy Manager proxy host | | `update.sh` says "uncommittete Änderungen" | Someone edited files directly on the server outside of a release — `git status` to see what, then commit/stash before updating |