Docker Compose
The production manifest is deploy/compose.prod.yml in the repository. Images come from ghcr.io/sianachi/nix/<image>:<full commit SHA>. The packages are public, so the host needs no registry login.
Services#
Service | Role |
|---|---|
| PostgreSQL 16 with pgvector: all durable state and search |
| Job transport between Core and the workers |
| S3-compatible storage for file bytes |
| Core: API, permissions, sign-in and search |
| Collaboration: live editing, history, import and export assembly |
| Imports and file inspection; also hosts the optional companion runtime |
| Native, Markdown, PDF and Word exports |
| Signed WebAssembly plugin runs |
| Google and Outlook calendar sync |
| Web Push delivery |
| Builds published websites ( |
| Serves published websites to anonymous readers ( |
| The web app, served by Caddy, which also routes the API, collaboration and storage |
| An optional tunnel connector for public ingress |
One-shot jobs prepare each release:
Job | Purpose |
|---|---|
| Core database migrations |
| Seeds the managed templates |
| Sets ownership on the data-protection key volume |
| Creates the storage bucket (maintenance profile) |
| Document schema migrations (maintenance profile) |
All five workers use the same worker image with a different role. Every service drops all Linux capabilities, cannot gain new privileges and, where it can, runs with a read-only filesystem.
Profiles#
Two Compose profiles keep optional services out of a normal up:
Profile | Services | How it is used |
|---|---|---|
|
| Run as one-shot jobs during a release |
|
| Set |
To enable Sites, set the two variables above and a distinct broker password for the sites worker, then point a wildcard hostname for *.<sites-domain> at the web container with the Host header left as sent. Caddy routes one-label hostnames under the sites domain to nix-sites-serve. nix-storage-init creates the sites bucket. See Sites and Configuration.
The release scripts#
deploy/compose/ holds the scripts that operate a Compose deployment. Each has a test that runs it against a stubbed Docker.
Script | Purpose |
|---|---|
| The one-command release: checks, backup, off-site push, deploy, smoke test and a ledger entry. See Upgrades |
| The deploy step on its own: validate, pull, stop writers, migrate, start in order, smoke test |
| A verified local backup, restored into an isolated database to prove it. See Backups |
| Pushes verified backups to an encrypted restic repository on S3-compatible storage |
| A nightly backup, push and retention run, with a systemd timer in |
| Previews which containers Compose will recreate, and stops if that includes PostgreSQL or the object store |
| Lists old release checkouts and backups, and deletes them only with |
| Builds local images from a commit, for the rare case you cannot use the published ones |
| The end-to-end smoke test, run through |
Ingress#
Caddy in nix-web listens on port 8090 on the private network and routes:
/api/*,/auth/*and/public/v1/auth/tokento Core;/collab/*to the collaboration service;/<bucket>/*to the object store;everything else to the web app.
Terminate TLS in front of it. The manifest includes a Cloudflare Tunnel connector, which needs no open inbound port: Cloudflare ends TLS and the connector forwards to nix-web on the private network; to use your own reverse proxy instead, add a Compose override that publishes the web container's port to the host's loopback interface and leaves out the tunnel:
# /srv/nix/compose.override.yml
services:
nix-web:
ports:
- "127.0.0.1:8090:8090"
cloudflared:
profiles: [disabled]
Pass it with a second -f after the main manifest. The manifest still requires CLOUDFLARE_TUNNEL_TOKEN to be set; any placeholder will do when the tunnel is disabled.
Then proxy https://<your-domain> to http://127.0.0.1:8090, passing the original Host header. WebSocket upgrades must be allowed for /collab/*.
First start#
Check out the release. Clone the repository and check out the full commit SHA you intend to run. The manifest, RabbitMQ configuration and seed files must match the images. Wait until CI has published images for that commit.
Write the environment file. Copy
deploy/compose.prod.env.exampleto a private path outside the checkout, set its mode to0600and replace every placeholder. See Configuration.install -m 600 deploy/compose.prod.env.example /srv/nix/production.env openssl ecparam -name prime256v1 -genkey -noout -out /srv/nix/core-access-token.pemSet up a shorthand for the Compose command:
export NIX_IMAGE_TAG=<full-commit-sha> NIX_WEB_IMAGE_TAG=<full-commit-sha> dc() { docker compose -p nix --env-file /srv/nix/production.env \ -f deploy/compose.prod.yml -f /srv/nix/compose.override.yml "$@"; }Leave out the override file if you use the tunnel.
Start the infrastructure.
dc up -d --wait postgres rabbitmq nix-versitygwCreate the database and roles. Nix uses three database roles:
nix_migratorowns the schema and runs migrations,nix_appis Core's runtime role, andnix_collabis the collaboration service's role. Neither runtime role may bypass row-level security. The repository's seed files create them with your passwords:dc exec -T postgres psql -v ON_ERROR_STOP=1 -U postgres -d postgres \ -v migrator_password='<migrator-password>' \ -v app_password='<app-password>' \ -v collab_password='<collab-password>' \ -v db_name=nix -f - < deploy/seed/seed.sql dc exec -T postgres psql -U postgres -d postgres -c 'CREATE DATABASE nix OWNER nix_migrator' dc exec -T postgres psql -v ON_ERROR_STOP=1 -U postgres -d nix \ -v db_name=nix -f - < deploy/seed/seed_database.sqlUse the same passwords in the connection strings in your environment file. Do not run the development or demo data seeds against a real deployment.
Prepare storage and run migrations.
dc --profile maintenance run --rm --no-deps nix-storage-init dc run --rm --no-deps nix-migrate dc run --rm --no-deps nix-template-presets dc run --rm --no-deps nix-api-init dc --profile maintenance run --rm --no-deps nix-collab-migrateStart everything.
dc up -d --waitSign in at
https://<your-domain>. Your first sign-in creates your personal workspace.
Checking it works#
A green health check only proves a process started. After a first start or an upgrade, check by hand:
sign in, reload a deep link and sign out;
edit the same note in two browsers and reload;
upload, open and download a file;
import a document and export a subtree as
.nix, PDF and Word.
nix-api has no Compose health check, because its image contains no shell. Its liveness route is /healthz on port 8080 on the private network. A 200 from the public /health path comes from the web container and does not prove Core is up.
Things never to do#
Never run
docker compose down -v,docker volume pruneor--remove-orphansagainst the Nix project. The named volumes hold your data and keys.Never print
docker compose configwith resolved secrets. Usedocker compose config --quietto validate.Never restore an old copy of a document's body in place under its existing ID. Installed apps hold local copies and would merge the removed content back. Use version history instead.