dorfteich/docs/self-hosting/README.md
Claude Fable 5 d2be1116bc
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m43s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 8m52s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m54s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m42s
CI / Import/export fidelity gate (push) Successful in 58s
Refresh the self-hosting guide for the first public release (#320)
- TAG guidance points at pinned release tags (e.g. v0.14.0) instead of
  the pre-release `test` tag; concrete curl commands fetch the three
  reference files.
- Backup wording (guide + .env.example) names all four data volumes in
  the restore set (uploads, plugins, custom fonts, branding).
- Updating section states the back-up-first step and links the update
  runbook.
- Pass the external-authentication variables (OIDC_*, AUTH_LOCAL_ENABLED,
  AUTH_PROXY_*) through the reference compose and document them in
  .env.example: they were documented in security.md but unreachable from
  .env. Empty values count as unset (app-config.service.ts), so the block
  is inert until configured.
- New guide section "External authentication (optional)"; neutral
  APP_BASE_URL example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aviRTgWCcAHUh1SBoxf6P
2026-08-03 12:34:30 +02:00

214 lines
9.7 KiB
Markdown

# 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 `caddy` profile (below).
- Outbound SMTP relay (optional at install time: the setup wizard can
configure it later, or you skip mail entirely at first).
## Install
1. Create a directory and fetch the reference files from the repository
(`deploy/compose/`): `docker-compose.yml`, `.env.example` — plus
`Caddyfile` if you want the `caddy` profile.
```sh
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
```
2. Edit `.env` — the minimum:
- `POSTGRES_PASSWORD`, `COLLAB_TOKEN_SECRET`: long random strings
(`openssl rand -base64 32`).
- `IMAGE_PREFIX=gitea.101010.cloud/stwaidele/dorfteich` and `TAG`: pin
the latest release tag (semver, e.g. `v0.14.0`) — the
[release list](https://gitea.101010.cloud/stwaidele/dorfteich/releases)
is authoritative. Moving tags like `test`/`int` track 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.example` with its
default and effect; nothing outside that file configures the stack.
- `.env.example` is the authoritative reference: real values live only
in your local `.env` and never enter the repository — a CI check
fails if any `.env` other than `.env.example` is ever tracked
(issue #198).
3. Start:
```sh
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`).
- **`caddy` profile:** set `DOMAIN=wiki.example.com` in `.env`; Caddy
publishes 80/443 and obtains Let's Encrypt certificates automatically
(both ports must be reachable from the internet). `DOMAIN=localhost`
issues an internal-CA certificate — for smoke tests only.
4. 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`).
## Updating
```sh
# 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`](../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>` (fetch `deploy/backup/restore.sh`
next to your compose file) — details in
`docs/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_):
1. In Nextcloud, create an **app password** for the account that should
hold the backups (Settings → Security → Devices & sessions).
2. 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 the `secrets` volume, never in the database.
3. 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`](../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](public-api.md).
## Health & troubleshooting
- `GET /api/v1/readyz` is the instance's own diagnosis. HTTP 503 =
database/migrations broken (the instance cannot serve). HTTP 200 with
`"status":"degraded"` = a warning-level check: `converter`/`renderer`
down (import/export/PDF degrade, everything else works) or `backup`
stale (last success older than 26 h). Each check carries a `detail`.
Monitor set: `deploy/monitoring.md`.
- Logs: `docker compose logs api` (or `web`, `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 403 `csrf_origin_mismatch``APP_BASE_URL` does not
match the URL in the browser (scheme and host must be identical).
E-mail links point at the wrong host → same variable.
- Wizard reappears after a restart → the database volume was not
persisted; never run without the `db-data` volume.
- `docker compose ps` shows `unhealthy` → 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:
```sh
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.