AI clients talk to the instance directly at /api/mcp — under the /api/ path (deviation from the issue's literal /mcp) so every existing reverse proxy already routes it; no deployment changes anywhere. - Transport: official @modelcontextprotocol/sdk server, STATELESS — each POST builds a fresh server+transport pair, no session store, replicas stay trivial; GET/DELETE answer 405. Auth per PAT bearer (#104 tokens), per-token rate limit (429 + Retry-After). - Own switches, independent of REST: instance mcp.enabled (admin settings, default off; off = 404, feature invisible) + pond setting mcpEnabled (pond-settings toggle, default off) — pinned independent in both directions by tests. - Tools (thin wrappers over the #104 services, same permission gates, audit-logged writes): list_ponds, list_pages, read_page, search, create_page, update_page (replace semantics through the collab-owned restore path — open editors converge), add_comment, list_labels, set_page_labels (exact replace), export_pond (link to the REST ZIP). Tool errors carry the api error codes; results carry stable slugs/ids. MCP resources stay the documented stage-2 stretch goal. - Deliberately on the SDK's low-level Server API with a hand-written tool table (mcp-tools.ts): the typed registerTool generics drove tsc out of memory in a program this size; manual Zod validation keeps the wire behavior explicit. - PublicApiService exposure filtering parameterized ('api' | 'mcp', shared pondFeatureEnabled helper) — one implementation, two switches. - Docs: "Connect Claude Code / MCP clients" section in public-api.md (claude mcp add one-liner + mcp-remote bridge for stdio clients). Verification: 8-test e2e pack driving the real MCP SDK client over Streamable HTTP against a listening api (initialize + tools/list, switch independence in both directions, anonymous/garbage 401, opt-in 404 semantics, page roundtrip incl. restore-NOTIFY, labels/comments, read scope blocked from writes with scope_required); live check through the web proxy against the seeded stack (tools list, create, read, update, search — LIVE CHECK PASSED); full api suite 61/61 files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
8.2 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 two reference files from the repository (
deploy/compose/):docker-compose.yml,.env.example— plusCaddyfileif you want thecaddyprofile.mkdir dorfteich && cd dorfteich # copy docker-compose.yml, .env.example (and Caddyfile) here cp .env.example .env && chmod 600 .env -
Edit
.env— the minimum:POSTGRES_PASSWORD,COLLAB_TOKEN_SECRET: long random strings (openssl rand -base64 32).IMAGE_PREFIX=gitea.101010.cloud/stwaidele/dorfteichandTAG: releases are semver tags (v1.2.3); until the first public release,testtracks the latest verified build.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.
-
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).
Updating
# 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 (the nightly sidecar gives you one at most 24 h old).
Backups & restore
Enabled by default (ADR 0015): the backup sidecar dumps the database and
archives the uploads/plugins volumes 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.
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. - 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.