Write the self-hosting guide and add the optional caddy TLS profile (#88)
All checks were successful
CD / Build and push images (push) Successful in 1m7s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m13s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m18s
CI / Import/export fidelity gate (push) Successful in 46s
All checks were successful
CD / Build and push images (push) Successful in 1m7s
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m13s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m18s
CI / Import/export fidelity gate (push) Successful in 46s
docs/self-hosting/README.md is the complete operator contract: install from the two reference files, first-run wizard walkthrough, update procedure with the one-release downgrade window, backup/restore with the sidecar, readyz-based troubleshooting (incl. the classic proxy/WebSocket and APP_BASE_URL/CSRF mistakes), and a build-from-source note; linked from the repository README; English-only by documented decision. The reference compose gains a `caddy` profile (new Caddyfile) that publishes 80/443 and terminates TLS via Let's Encrypt for $DOMAIN — localhost uses Caddy's internal CA for smoke tests. deploy/self-hosting-verify.sh scripts the clean-machine test: a fresh directory with only the published files boots to the wizard answering over TLS, then removes itself; verified green on the stage host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
parent
6e99cb35fc
commit
4b55fb92ac
@ -29,13 +29,15 @@ offline support.
|
|||||||
- **Plugins** — sandboxed extensions (custom blocks, styles, page tools)
|
- **Plugins** — sandboxed extensions (custom blocks, styles, page tools)
|
||||||
installable at runtime without redeploying the instance.
|
installable at runtime without redeploying the instance.
|
||||||
- **Self-hosting first** — a single `docker compose up` plus a guided
|
- **Self-hosting first** — a single `docker compose up` plus a guided
|
||||||
first-run setup wizard yields a working instance.
|
first-run setup wizard yields a working instance. Start here:
|
||||||
|
[`docs/self-hosting/README.md`](docs/self-hosting/README.md).
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
| Path | Contents |
|
| Path | Contents |
|
||||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `docs/architecture/` | Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations |
|
| `docs/architecture/` | Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations |
|
||||||
|
| `docs/self-hosting/` | Install, update, backup, and troubleshooting guide for running your own instance |
|
||||||
| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds |
|
| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds |
|
||||||
| `packages/` | Shared packages (types, permission logic, plugin SDK) |
|
| `packages/` | Shared packages (types, permission logic, plugin SDK) |
|
||||||
| `deploy/` | Docker Compose stacks and deployment tooling |
|
| `deploy/` | Docker Compose stacks and deployment tooling |
|
||||||
|
|||||||
@ -48,6 +48,17 @@ SMTP_USER=wiki@example.com
|
|||||||
SMTP_PASS=change-me
|
SMTP_PASS=change-me
|
||||||
SMTP_FROM=Dorfteich <wiki@example.com>
|
SMTP_FROM=Dorfteich <wiki@example.com>
|
||||||
|
|
||||||
|
# --- optional TLS ingress (`caddy` profile, issue #88) -------------------------
|
||||||
|
# Only when you have no reverse proxy of your own: start with
|
||||||
|
# `docker compose --profile caddy up -d`. Caddy terminates TLS for DOMAIN
|
||||||
|
# via Let's Encrypt (80+443 must be reachable from the internet; keep
|
||||||
|
# APP_BASE_URL=https://<DOMAIN> in sync). The `localhost` default issues
|
||||||
|
# an internal-CA certificate instead — good for smoke tests only.
|
||||||
|
#DOMAIN=wiki.example.com
|
||||||
|
# Published ports; change only when 80/443 are taken on the host.
|
||||||
|
#CADDY_HTTP_PORT=80
|
||||||
|
#CADDY_HTTPS_PORT=443
|
||||||
|
|
||||||
# --- backups (ADR 0015, issue #83) --------------------------------------------
|
# --- backups (ADR 0015, issue #83) --------------------------------------------
|
||||||
# The backup sidecar dumps the database and archives the uploads/plugins
|
# The backup sidecar dumps the database and archives the uploads/plugins
|
||||||
# volumes nightly onto the `backups` volume; restore via
|
# volumes nightly onto the `backups` volume; restore via
|
||||||
|
|||||||
21
deploy/compose/Caddyfile
Normal file
21
deploy/compose/Caddyfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Ingress for the optional `caddy` compose profile (issue #88).
|
||||||
|
# $DOMAIN comes from .env; a real domain gets automatic Let's Encrypt
|
||||||
|
# certificates (ports 80+443 must be reachable from the internet), the
|
||||||
|
# `localhost` default uses Caddy's internal CA. Routing mirrors
|
||||||
|
# deployment.md: /api → api, /collab (WebSocket) → collab, rest → web.
|
||||||
|
|
||||||
|
{$DOMAIN:localhost} {
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy api:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /collab* {
|
||||||
|
reverse_proxy collab:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy web:8080
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -223,6 +223,30 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
<<: *logging
|
<<: *logging
|
||||||
|
|
||||||
|
# Optional TLS ingress (issue #88): for self-hosters without their own
|
||||||
|
# reverse proxy. `docker compose --profile caddy up -d` publishes 80/443
|
||||||
|
# and terminates TLS via Let's Encrypt for $DOMAIN (set it in .env; the
|
||||||
|
# `localhost` default uses Caddy's internal CA — handy for smoke tests).
|
||||||
|
# Instances behind an existing host proxy simply never enable the profile.
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.10-alpine
|
||||||
|
profiles: [caddy]
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '${CADDY_HTTP_PORT:-80}:80'
|
||||||
|
- '${CADDY_HTTPS_PORT:-443}:443'
|
||||||
|
environment:
|
||||||
|
DOMAIN: ${DOMAIN:-localhost}
|
||||||
|
networks: [frontend]
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy-data:/data
|
||||||
|
- caddy-config:/config
|
||||||
|
depends_on:
|
||||||
|
web:
|
||||||
|
condition: service_started
|
||||||
|
<<: *logging
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
frontend:
|
frontend:
|
||||||
internal:
|
internal:
|
||||||
@ -233,3 +257,6 @@ volumes:
|
|||||||
plugins:
|
plugins:
|
||||||
secrets:
|
secrets:
|
||||||
backups:
|
backups:
|
||||||
|
# Only used by the optional `caddy` profile (certificates + state).
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
|
|||||||
76
deploy/self-hosting-verify.sh
Executable file
76
deploy/self-hosting-verify.sh
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Scripted clean-machine install test for the self-hosting guide (issue #88):
|
||||||
|
# proves that a fresh directory containing ONLY the published reference files
|
||||||
|
# (docker-compose.yml, Caddyfile, .env.example) boots to a working first-run
|
||||||
|
# wizard behind the caddy profile's TLS. Uses its own compose project name,
|
||||||
|
# high ports, and DOMAIN=localhost (internal-CA certificate), then removes
|
||||||
|
# everything — safe to run on a host with live stacks.
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# IMAGE_PREFIX (default gitea.101010.cloud/stwaidele/dorfteich)
|
||||||
|
# TAG (default test; a release = its semver tag)
|
||||||
|
# HTTPS_PORT (default 8443) HTTP_PORT (default 8480)
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
IMAGE_PREFIX="${IMAGE_PREFIX:-gitea.101010.cloud/stwaidele/dorfteich}"
|
||||||
|
TAG="${TAG:-test}"
|
||||||
|
HTTPS_PORT="${HTTPS_PORT:-8443}"
|
||||||
|
HTTP_PORT="${HTTP_PORT:-8480}"
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
PROJECT="dorfteich-selfhost-verify-$(date +%s)"
|
||||||
|
DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
log() { echo "self-hosting-verify: $*"; }
|
||||||
|
fail() { echo "self-hosting-verify: FAILED — $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
log "tearing down $PROJECT"
|
||||||
|
(cd "$DIR" && docker compose --profile caddy down -v --remove-orphans >/dev/null 2>&1) || true
|
||||||
|
rm -rf "$DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# --- the guide's install steps, scripted -------------------------------------
|
||||||
|
cp "$HERE/compose/docker-compose.yml" "$HERE/compose/Caddyfile" "$DIR/"
|
||||||
|
cp "$HERE/compose/.env.example" "$DIR/.env"
|
||||||
|
chmod 600 "$DIR/.env"
|
||||||
|
|
||||||
|
edit() { sed -i.bak "s|^#*$1=.*|$1=$2|" "$DIR/.env" && rm "$DIR/.env.bak"; }
|
||||||
|
edit POSTGRES_PASSWORD "verify-$(date +%s%N | tail -c 13)"
|
||||||
|
edit COLLAB_TOKEN_SECRET "verify-secret-$(date +%s%N)"
|
||||||
|
edit IMAGE_PREFIX "$IMAGE_PREFIX"
|
||||||
|
edit TAG "$TAG"
|
||||||
|
edit COMPOSE_PROJECT_NAME "$PROJECT"
|
||||||
|
edit WEB_PORT 0 && edit API_PORT 0 && edit COLLAB_PORT 0 # ephemeral host ports
|
||||||
|
edit APP_BASE_URL "https://localhost:$HTTPS_PORT"
|
||||||
|
edit DOMAIN localhost
|
||||||
|
edit CADDY_HTTP_PORT "$HTTP_PORT"
|
||||||
|
edit CADDY_HTTPS_PORT "$HTTPS_PORT"
|
||||||
|
# Mail stays unconfigured — exactly the guide's "skip SMTP for now" path.
|
||||||
|
edit SMTP_HOST "" && edit SMTP_PORT "" && edit SMTP_SECURE ""
|
||||||
|
edit SMTP_USER "" && edit SMTP_PASS "" && edit SMTP_FROM ""
|
||||||
|
|
||||||
|
log "starting $PROJECT from $DIR (images $IMAGE_PREFIX-*:$TAG)"
|
||||||
|
(cd "$DIR" && docker compose --profile caddy pull --quiet && docker compose --profile caddy up -d)
|
||||||
|
|
||||||
|
# --- the guide's promise: the wizard answers over TLS ------------------------
|
||||||
|
SETUP=""
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
SETUP=$(curl -sk "https://localhost:$HTTPS_PORT/api/v1/setup" || true)
|
||||||
|
case "$SETUP" in *'"status":"required"'*) break ;; esac
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
case "$SETUP" in
|
||||||
|
*'"status":"required"'*) log "wizard reachable over TLS: $SETUP" ;;
|
||||||
|
*) (cd "$DIR" && docker compose logs api caddy | tail -40); fail "wizard never answered: $SETUP" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo | openssl s_client -connect "localhost:$HTTPS_PORT" -servername localhost 2>/dev/null \
|
||||||
|
| grep -q 'Caddy Local Authority' || fail "TLS certificate is not Caddy-issued"
|
||||||
|
log "TLS certificate issued by Caddy's internal CA (a real DOMAIN gets Let's Encrypt)"
|
||||||
|
|
||||||
|
curl -sk "https://localhost:$HTTPS_PORT/" | grep -qi '<div id="root">' \
|
||||||
|
|| fail "web app shell not served through the ingress"
|
||||||
|
log "SPA shell served through the ingress"
|
||||||
|
|
||||||
|
log "OK — a clean install following only the guide reaches the working wizard"
|
||||||
145
docs/self-hosting/README.md
Normal file
145
docs/self-hosting/README.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# 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 two 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
|
||||||
|
# copy docker-compose.yml, .env.example (and Caddyfile) here
|
||||||
|
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`:
|
||||||
|
releases are semver tags (`v1.2.3`); until the first public release,
|
||||||
|
`test` tracks 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.example` with its
|
||||||
|
default and effect; nothing outside that file configures the stack.
|
||||||
|
|
||||||
|
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
|
||||||
|
# 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`
|
||||||
|
- 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`.
|
||||||
|
- Copy the `backups` volume off the host regularly; a backup on the same
|
||||||
|
disk protects against mistakes, not against losing the host.
|
||||||
|
|
||||||
|
## 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.
|
||||||
Loading…
Reference in New Issue
Block a user