Two findings from Stefan's manual clean install per the guide, both ending in an api restart loop that was hard to diagnose: - #324: the guide recommended `openssl rand -base64 32` for POSTGRES_PASSWORD, but the compose interpolates the password unescaped into DATABASE_URL — base64's `/`, `+`, `=` break the URL. Misleadingly, db stays healthy (it gets the password as a plain env var) while api/collab/backup crash. Guide and .env.example now recommend `openssl rand -hex 24` for both secrets and say why; Troubleshooting gained the symptom line. - #325: SETUP_ADMIN_PASSWORD's minimum (10 chars, packages/shared/src/auth.ts) was undocumented, and a violation crashed the boot with a raw ZodError naming schema fields and i18n keys. Failing the boot stays — deliberately, no half-seeded instance — but preseedFromEnv now translates validation errors into operator terms ("Pre-seeding failed: SETUP_ADMIN_PASSWORD must be at least 10 characters. Fix .env and recreate the api container."). Documented in the guide's first-run section, .env.example, and Troubleshooting; new test pins the message and that nothing is half-seeded afterwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aviRTgWCcAHUh1SBoxf6P
11 KiB
Self-hosting Dorfteich
Everything you need to install, run, update, and back up your own Dorfteich with Docker — this guide is the complete contract: if a step here does not work, that is a bug (issue #88).
Docs are English-only by decision (issue #88): the product UI is fully localized (de/en), operator documentation is not — one authoritative text beats two drifting ones.
Requirements
- Docker Engine with the Compose plugin (
docker compose version≥ 2.20). - 2 GB RAM, ~2 GB disk for images plus room for your content and backups.
- A domain pointing at the host — TLS via your own reverse proxy or the
bundled
caddyprofile (below). - Outbound SMTP relay (optional at install time: the setup wizard can configure it later, or you skip mail entirely at first).
Install
-
Create a directory and fetch the reference files from the repository (
deploy/compose/):docker-compose.yml,.env.example— plusCaddyfileif you want thecaddyprofile.mkdir dorfteich && cd dorfteich base=https://gitea.101010.cloud/stwaidele/dorfteich/raw/branch/main/deploy/compose curl -fsSLO "$base/docker-compose.yml" curl -fsSLO "$base/.env.example" curl -fsSLO "$base/Caddyfile" # only for the caddy profile cp .env.example .env && chmod 600 .env -
Edit
.env— the minimum:POSTGRES_PASSWORD,COLLAB_TOKEN_SECRET: long random strings — generate both withopenssl rand -hex 24. Stick to URL-safe characters for the database password (hex is): it is interpolated into a connection URL, and a/,+or=from base64 output breaks it in a confusing way (db healthy, everything else restart-looping — see Troubleshooting).IMAGE_PREFIX=gitea.101010.cloud/stwaidele/dorfteichandTAG: pin the latest release tag (semver, e.g.v0.14.0) — the release list is authoritative. Moving tags liketest/inttrack our stages and are not meant for third-party installs.APP_BASE_URL=https://wiki.example.com— must be exactly what browsers will use; e-mail links and the CSRF origin check derive from it.- Every other variable is documented inline in
.env.examplewith its default and effect; nothing outside that file configures the stack. .env.exampleis the authoritative reference: real values live only in your local.envand never enter the repository — a CI check fails if any.envother than.env.exampleis ever tracked (issue #198).
-
Start:
docker compose up -d # behind your own reverse proxy docker compose --profile caddy up -d # or with the bundled TLS ingress- Own proxy: route
/*→127.0.0.1:$WEB_PORT,/api/*→$API_PORT,/collab*→$COLLAB_PORT(WebSocket upgrade required on/collab). caddyprofile: setDOMAIN=wiki.example.comin.env; Caddy publishes 80/443 and obtains Let's Encrypt certificates automatically (both ports must be reachable from the internet).DOMAIN=localhostissues an internal-CA certificate — for smoke tests only.
- Own proxy: route
-
Open
https://your-domain/— a fresh instance shows the first-run setup wizard.
First-run wizard
Six steps, all in the browser (issue #80/#81): language → the Site-Admin
account (created verified, you are signed in immediately) → instance name
and default language → SMTP relay (Save runs a live test and sends a
test mail to you; Skip leaves mail unconfigured — sign-up verification
will not work until an admin adds a relay) → registration mode
(open/closed) → summary + finish. The wizard locks itself permanently on
completion. Until it completes, the api answers everything but the wizard
and health endpoints with 503 setup_required — that is not an error.
Unattended installs skip the wizard by pre-seeding: set the
SETUP_ADMIN_* variables in .env before the first start (see
.env.example). The same validation as in the wizard applies —
SETUP_ADMIN_PASSWORD needs at least 10 characters — and an invalid
value deliberately fails the boot with a message naming the variable
(a half-seeded instance would be harder to diagnose).
Updating
# 1. take a backup first — the pre-update set is the guaranteed way back:
# Admin → System → "Back up now", or:
docker compose run --rm -e BACKUP_RUN_ONCE=1 backup
# 2. edit .env: TAG=v1.3.0
docker compose pull && docker compose up -d
Database migrations run automatically at api start. Release notes flag
releases with a migration label and any manual steps. Downgrade
window: one minor release — TAG back + pull + up -d is supported
one step back; further back, restore the backup taken before the update
instead. Rollback paths — including what to do when a migration itself
fails — are in
docs/operations/update-runbook.md.
Backups & restore
Enabled by default (ADR 0015): the backup sidecar dumps the database and
archives the data volumes (uploads, plugins, custom fonts, branding)
nightly at BACKUP_TIME onto the backups volume, prunes by
BACKUP_RETENTION_DAYS, writes status.json, and — with
BACKUP_MAIL_TO set — mails you on failure.
- On-demand backup:
docker compose run --rm -e BACKUP_RUN_ONCE=1 backup, or the Back up now button under Admin → System. - List sets:
docker compose exec backup ls /backups - Restore:
./restore.sh <backup-id>(fetchdeploy/backup/restore.shnext to your compose file) — details indocs/operations/restore-runbook.md.
Off-host copies to a Nextcloud
Get the backups off the host — a backup on the same disk protects against mistakes, not against losing the host. Any Nextcloud you can reach works as the target; configure it entirely in the admin UI (Admin → System → Backups):
- In Nextcloud, create an app password for the account that should hold the backups (Settings → Security → Devices & sessions).
- In Dorfteich, enable Upload backups to Nextcloud, enter the plain
Nextcloud address (e.g.
https://cloud.example.com), the username, the app password and a folder, and use Test connection — it verifies the credentials and creates the folder. The password is kept in the secret store on thesecretsvolume, never in the database. - Pick the upload schedule (after every nightly backup, weekly, or manual only) and the retention for both sides. After each successful upload, old remote bundles beyond the retention are pruned — never the newest one.
Each upload is ONE self-contained archive
(dorfteich-backup-<id>.tar.gz = database dump + files archive +
manifest) — everything needed to rebuild the instance after total loss.
readyz warns (backup_remote check) when the off-host copy grows stale,
and upload failures alert through the backup failure mail.
Restore from the admin UI: Admin → System → Backups → Restore lists
local and Nextcloud sets. Restoring asks you to re-type the backup id,
then the instance enters maintenance mode (everything answers 503 plus a
status page), restores itself through the backup sidecar, and restarts.
If the app itself is gone, use the operator path in
docs/operations/restore-runbook.md instead — it documents fetching a
bundle from Nextcloud by hand.
External authentication (optional)
Local username/password accounts work out of the box — nothing to
configure. Deployments with an existing identity provider can add OpenID
Connect login (OIDC_ISSUER + OIDC_CLIENT_ID in .env enable it), let
an authenticating reverse proxy assert identities (AUTH_PROXY_*), and —
after the first-run setup is complete — turn local credentials off
entirely with AUTH_LOCAL_ENABLED=false. All of this is deploy-level by
design: a Site Admin cannot change it from the UI. The variables are
documented in .env.example; semantics and the trust model are in
docs/architecture/security.md
(§External authentication).
Public REST API
Scripts and integrations can talk to the instance through a
token-authenticated API at /api/public/v1, and MCP clients (Claude Code
and friends) through the built-in MCP endpoint at /api/mcp — both off by
default, enabled per instance and per pond. Details, token walkthrough,
and the OpenAPI document: public-api.md.
Health & troubleshooting
GET /api/v1/readyzis the instance's own diagnosis. HTTP 503 = database/migrations broken (the instance cannot serve). HTTP 200 with"status":"degraded"= a warning-level check:converter/rendererdown (import/export/PDF degrade, everything else works) orbackupstale (last success older than 26 h). Each check carries adetail. Monitor set:deploy/monitoring.md.- Logs:
docker compose logs api(orweb,collab,backup,db) — structured JSON, rotated by Docker. - Proxy pitfalls: editor never connects / "offline" although the page
loads → the proxy does not upgrade WebSockets on
/collab. All mutations fail with 403csrf_origin_mismatch→APP_BASE_URLdoes not match the URL in the browser (scheme and host must be identical). E-mail links point at the wrong host → same variable. - api, collab and backup restart-looping while
dbis healthy →POSTGRES_PASSWORDcontains characters that break the connection URL (base64's/,+,=); regenerate withopenssl rand -hex 24and recreate the stack. The db container looks fine because only its clients build a URL from the password. - api restart-looping right after the first start with a
Pre-seeding failed(orvalidation.password.tooShort) message →SETUP_ADMIN_PASSWORDis shorter than 10 characters; fix.envand recreate the api container. - Wizard reappears after a restart → the database volume was not
persisted; never run without the
db-datavolume. docker compose psshowsunhealthy→ that container's liveness check fails; a degraded readyz alone never marks containers unhealthy and never restarts anything.
Building from source instead
Clone the repository and build the images locally — the reference compose carries the build contexts already:
docker compose build && docker compose up -d
Same layout, same volumes; you trade the registry pull for a local toolchain (Node 22 build stages run inside Docker, nothing else needed).
Verified install
The guide is verified by a scripted clean-machine run
(deploy/self-hosting-verify.sh): fresh directory, reference compose +
.env.example only, --profile caddy with an internal-CA certificate,
asserting that the wizard answers over TLS. Run it yourself on any Docker
host — it uses its own compose project name and high ports, then removes
everything.