All checks were successful
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
230 lines
11 KiB
Markdown
230 lines
11 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 —
|
|
generate both with `openssl 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/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`). 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
|
|
|
|
```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.
|
|
- api, collab **and** backup restart-looping while `db` is healthy →
|
|
`POSTGRES_PASSWORD` contains characters that break the connection URL
|
|
(base64's `/`, `+`, `=`); regenerate with `openssl rand -hex 24` and
|
|
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` (or `validation.password.tooShort`) message →
|
|
`SETUP_ADMIN_PASSWORD` is shorter than 10 characters; fix `.env` and
|
|
recreate the api container.
|
|
- 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.
|