dorfteich/docs/architecture/deployment.md
Claude Opus 4.8 8316c617d2
All checks were successful
CD / Build and push images (push) Successful in 2m36s
CI / Lint, typecheck, test (push) Successful in 1m50s
CI / Auth e2e pack (push) Successful in 1m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Add collaboration server skeleton (Hocuspocus) with health, container, and CI/CD (#33)
Bootstrap apps/collab as a Hocuspocus WebSocket server (ADR 0003):
- pino JSON logging (service=collab) and shared Zod env validation
  (collabEnvSchema); structured connection open/close logs.
- /healthz endpoint (process liveness + PostgreSQL ping) served via the
  onRequest hook, matching the container-internal path and the proxied
  /collab/healthz path; any WebSocket handshake is accepted for now
  (authentication arrives with #34, persistence with #35).
- Dockerfile (ESM workspace build) and a compose service on the frontend
  and internal networks with a healthcheck; dev overlay service and a new
  COLLAB_PORT variable.
- CD builds, pushes, and promotes the collab image; CI builds it on PRs;
  the smoke suite asserts /collab/healthz through the reverse proxy.
- deployment.md/stages.md: proxy routing, per-stage COLLAB_PORT, checklist.

Closes #33

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:53:44 +02:00

108 lines
6.2 KiB
Markdown

# Deployment architecture
Four stages, one Compose definition. Foundational decisions: ADR 0014
(CI/CD), ADR 0015 (backup), kickoff topology decision (Dev local on the
developer's machine; Test and Int on the operator's VPS `188.245.116.44`;
the Prod host is decided at go-live — the architecture must keep that
choice and any later move cheap). DNS status: `*.dorfteich.online` and
`*.dorfteich.cloud` already point to the VPS.
## The Compose stack
Every stage (and every self-hosted instance) runs the same services:
| Service | Image | Notes |
| ----------- | ------------------------------------ | --------------------------------------------------------------- |
| `web` | `dorfteich-web` | nginx: SPA assets, fonts; SPA fallback routing |
| `api` | `dorfteich-api` | NestJS; runs `prisma migrate deploy` on start |
| `collab` | `dorfteich-collab` | Hocuspocus WebSocket server |
| `db` | `postgres:<pinned>` | volume `db-data` |
| `pandoc` | `pandoc/core:<pinned>` (server mode) | internal only |
| `gotenberg` | `gotenberg/gotenberg:<pinned>` | internal only |
| `backup` | `dorfteich-backup` | cron sidecar: pg_dump, volume archive, prune, mirror (ADR 0015) |
Volumes: `db-data`, `uploads` (uploads + installed plugins), `backups`.
Networks: `frontend` (reverse proxy ↔ web/api/collab) and `internal`
(api/collab ↔ db/pandoc/gotenberg); db and converters are never exposed.
Ingress is a host-level reverse proxy (existing Caddy/Traefik/nginx on the
host), routing:
```
/ → web
/api/ → api
/collab → collab (WebSocket upgrade required)
/media/ → api (permission-checked file streaming)
```
The proxy forwards `/collab` without stripping the prefix, so the collab
service answers its health probe at `/collab/healthz` externally and at
`/healthz` for the container-internal Docker healthcheck.
Self-hosters without a proxy can enable the optional `caddy` Compose profile
(bundled Caddy with automatic TLS).
## Stages
| Stage | Where | Domain | Purpose | Data |
| -------- | ----------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------- |
| **Dev** | contributor machine (e.g. the operator's MacBook), Docker Desktop | `localhost` | feature work; hot reload via `compose.dev.yml` overlay (source mounts, vite dev server) | fixtures/seed script |
| **Test** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-test/` | `test.dorfteich.cloud` | auto-deploy target of `main`; e2e suite runs here | reset-able; seeded |
| **Int** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-int/` | `int.dorfteich.cloud` | stable preview; manual/exploratory testing; release candidates | persistent test data |
| **Prod** | host decided at go-live (M8): the VPS or a dedicated host | `dorfteich.online` | public flagship instance | real data; full backup + mirror |
Stage layout follows the operator's Docker host convention:
compose file + `.env` under `/home/DOCKER/dorfteich-<stage>/`, bulk data
volumes under `/home/RAID/DOCKER/dorfteich-<stage>/` (bind-mounted).
**Prod relocation readiness** (kickoff requirement): all state lives in the
three volumes + `.env`; the documented move procedure is: stop stack →
final backup → restore backup set on the new host → switch DNS. The backup
sidecar's restore runbook doubles as the migration procedure, and Test
restore drills (ADR 0015) keep it honest.
## Configuration
- One `.env` per stage (never in git; `.env.example` in the repo documents
every variable): database credentials, `APP_BASE_URL`, collab token
signing key, SMTP settings, stage name shown in the UI for non-Prod.
- First-run **setup wizard** (kickoff decision): when the API starts against
an empty database it exposes only `/setup` (create Site Admin account,
SMTP, instance name/locale, registration mode); the wizard locks itself
after completion. `.env` can pre-seed these for automated deploys
(Test/Int use exactly that).
## Pipeline (ADR 0014, concrete)
```mermaid
flowchart LR
PR[PR: lint + typecheck + unit + build] -->|merge| M[main]
M --> B[build images :sha]
B --> DT[deploy Test]
DT --> E2E[Playwright e2e vs Test]
E2E -->|green| DI[deploy Int - tag int]
DI --> REL{manual: tag vX.Y.Z + approval}
REL --> DP[deploy Prod - semver tag]
```
- Deploy jobs SSH into the stage directory and run
`docker compose pull && docker compose up -d`; migrations apply on api
start. Rollback = re-deploy the previous tag (migrations must be
backward-compatible one release back — contributor rule for schema
stories).
- The e2e suite is the Int-promotion gate; flaky tests are defects.
- Release notes are generated from merged PR titles; releases with
data-affecting migrations are labeled `migration` and called out.
## Self-hosting distribution
- Published artifacts per release: versioned images in the Gitea registry
(mirrored to a public registry at first public release), a reference
`docker-compose.yml` + `.env.example`, and the install/update/backup
guide (`docs/self-hosting/`, written as part of the docs milestone).
- Minimum requirements: Docker + Compose, 2 GB RAM, a domain (TLS via own
proxy or the `caddy` profile). Setup = compose up + browser wizard.
- Updates: `docker compose pull && up -d` on a new semver tag; migrations
run automatically; the release notes flag anything manual. Downgrades are
supported one release back.