dorfteich/deploy/stages.md
Claude Opus 4.8 d4ebcfcfbe
All checks were successful
CD / Build and push images (push) Successful in 2m45s
CI / Lint, typecheck, test (push) Successful in 1m56s
CI / Auth e2e pack (push) Successful in 2m1s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 12s
Add collaboration token issuance and connection authentication (#34)
The api mints a short-lived (60 s) HS256 JWT per page open after an interim
permission check; the collab server authenticates every connection with it
(ADR 0003/0007 — the only JWTs in the system).

- packages/shared: browser-safe token schema/types in `collab-token`, and the
  Node `crypto` sign/verify in `token-crypto` behind its own subpath export
  (`@dorfteich/shared/token-crypto`) so the web bundle never pulls in
  `node:crypto`. Only HS256 is produced/accepted; the signature is checked in
  constant time before any untrusted field is read.
- api: `GET /pages/:id/collab-token` (auth-required) returns
  {token, mode, expiresInSeconds}; `mode` is rw/ro via the interim access
  service; issuance is logged at debug level without the token value.
- collab: `onAuthenticate` verifies the token, checks the pageId matches the
  document name, stores {userId, mode} context, and enforces `ro` via
  Hocuspocus' read-only connection flag. Hocuspocus' own signal handling is
  disabled so index.ts remains the single shutdown owner.
- Shared COLLAB_TOKEN_SECRET env for api + collab (compose, dev overlay,
  .env.example, stage docs); a dev default keeps native dev/test/CI running.

Tests: shared token round-trip/rejection; api endpoint e2e (auth required,
claims, 404 for non-members/unknown ids); collab integration via
HocuspocusProvider (valid token connects; expired/tampered/mismatched-page/
wrong-secret rejected; read-only writes dropped, verified with two clients).

Closes #34

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:52:19 +02:00

5.7 KiB

Stage provisioning on the VPS (188.245.116.44)

Test and Int run as Compose stacks on the operator's VPS; DNS for *.dorfteich.cloud already points there (deployment.md §Stages). Steps marked [root] need host root access and are executed by the repo owner; everything else can be done by CI or a deploy user.

Overview

Stage Directory Domain Ports (localhost)
Test /home/DOCKER/dorfteich-test/ test.dorfteich.cloud web 8100, api 8101, collab 8102
Int /home/DOCKER/dorfteich-int/ int.dorfteich.cloud web 8110, api 8111, collab 8112

1. Stage directories [root]

for stage in test int; do
  mkdir -p /home/DOCKER/dorfteich-$stage
  mkdir -p /home/RAID/DOCKER/dorfteich-$stage   # bulk data, if RAID exists on this host
done

Copy deploy/compose/docker-compose.yml and .env.example.env into each stage directory. Set per stage in .env (mode 600):

  • POSTGRES_PASSWORD: unique random value per stage
  • COLLAB_TOKEN_SECRET: long random value per stage (openssl rand -base64 32); signs/verifies the collaboration tokens (issue #34). The api and collab services read the same value from this one variable.
  • COMPOSE_PROJECT_NAME: dorfteich-test / dorfteich-int
  • WEB_PORT/API_PORT/COLLAB_PORT: 8100/8101/8102 (test), 8110/8111/8112 (int). COLLAB_PORT must be set per stage — both stacks share this host, so the compose default (8102) would make the Int collab container collide with Test's; Int needs COLLAB_PORT=8112.
  • IMAGE_PREFIX=gitea.101010.cloud/stwaidele/dorfteich
  • TAG: managed by the CD pipeline (<git-sha> on test, int on int)
  • APP_BASE_URL: https://test.dorfteich.cloud / https://int.dorfteich.cloud — e-mail links and the CSRF origin check depend on it
  • SMTP_HOST/SMTP_PORT/SMTP_SECURE/SMTP_USER/SMTP_PASS/SMTP_FROM: real relay credentials (both non-prod stages share one mailbox); without them, signup/reset mails queue up and fail

Fixture accounts on the stages are created with the regular seed, but with stage-specific passwords (never the public dev password). Always pass both override variables when re-seeding a stage — the seed re-hashes credentials on every run, so omitting them silently resets the stage accounts to the public dev password:

FIXTURE_ADMIN_PASSWORD=FIXTURE_USER_PASSWORD=\
  DATABASE_URL=postgresql://dorfteich:…@localhost:<tunnel-port>/dorfteich \
  pnpm --filter @dorfteich/api db:seed

(The stage db is not published; tunnel to the db container, e.g. ssh -L 15432:<db-container-ip>:5432 root@188.245.116.44.)

2. Reverse proxy vhosts [root]

Both vhosts terminate TLS and route by path; WebSocket upgrade on /collab is required from milestone M3 on, configure it now. Caddy example:

test.dorfteich.cloud {
    handle /api/* {
        reverse_proxy 127.0.0.1:8101
    }
    handle /collab* {
        reverse_proxy 127.0.0.1:8102   # Hocuspocus collab (issue #33); Caddy passes the WebSocket upgrade through automatically
    }
    handle {
        reverse_proxy 127.0.0.1:8100
    }
}

(nginx equivalent: proxy_pass per location; for /collab add proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";.)

Int: same block with int.dorfteich.cloud and ports 8110/8111/8112.

3. Gitea act_runner [root]

The CI/CD workflows (.gitea/workflows/) need one act_runner on the VPS with Docker access and the ubuntu-latest label:

# 1. Download act_runner (https://gitea.com/gitea/act_runner/releases)
# 2. Registration token: Gitea → Site/Repo Settings → Actions → Runners
act_runner register \
  --instance https://gitea.101010.cloud \
  --token <REGISTRATION_TOKEN> \
  --name vps-dorfteich \
  --labels ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04
# 3. Run as a systemd service (act_runner daemon), user in the docker group.

4. Deploy user and SSH keys

The CD workflow (issue #8) deploys via SSH: ssh deploy@188.245.116.44 'cd /home/DOCKER/dorfteich-test && docker compose pull && docker compose up -d'.

  • [root] Create a deploy user (or reuse an existing deployment user), member of the docker group, owning the stage directories.
  • Generate one ed25519 keypair per stage; public keys into deploy's authorized_keys (optionally with a command= restriction to the compose command), private keys become the repository secrets DEPLOY_SSH_KEY_TEST / DEPLOY_SSH_KEY_INT.

5. Registry access

The pipeline pushes images to the Gitea container registry (gitea.101010.cloud/stwaidele/dorfteich-{web,api,collab}):

  • Repository secret REGISTRY_TOKEN: a Gitea access token with write:package scope (owner stwaidele or a CI account).
  • On the VPS, docker login gitea.101010.cloud for the deploy user with a read:package token, so compose pull works.

6. Verification checklist

  • https://test.dorfteich.cloud/healthzok
  • https://test.dorfteich.cloud/api/v1/readyz{"status":"ok",…}
  • https://test.dorfteich.cloud/collab/healthz{"status":"ok","service":"collab",…}
  • same for int
  • runner shows online under Gitea → Settings → Actions → Runners
  • a test workflow run executes on the runner
  • .env files are mode 600, owned by deploy

Provisioning log

  • 2026-07-05: Test/Int stage directories, .env files, Caddy vhosts (TLS live), deploy user, act_runner (v0.6.1, systemd) and registry login provisioned; Gitea Actions enabled instance-wide (app.ini on BASEL, backup kept). First pipeline run = this commit.