Skip to main content

Operate a Docker Compose deployment

Norn's recoverable state spans PostgreSQL, the attachment volume, and the encryption key. Operate them as one system. Every command on this page runs from the directory holding compose.yaml.

Backups

A complete backup contains:

  • a consistent PostgreSQL backup
  • the contents of the attachment volume
  • .env, which holds NORN_SECURITY_ENCRYPTION_KEY and the service passwords
  • compose.yaml and compose.override.yaml
  • Valkey data when retaining sessions and queued work is part of the recovery objective

PostgreSQL holds attachment metadata while the volume holds the bytes. Align their backup times and test restoring both together.

Dump the bundled database with PostgreSQL's own tooling rather than copying a live volume:

docker compose exec -T postgres \
pg_dump --username norn --dbname norn --format=custom > norn.dump

Archive the attachment volume:

docker run --rm \
--volume norn_blob-data:/blobs:ro \
--volume "$PWD:/backup" \
busybox:1.37.0 tar czf /backup/norn-blobs.tar.gz -C /blobs .

.env is a secret. Copy it to an encrypted store with restricted access, never into the same unprotected place as the dump. A backup that omits it cannot decrypt the credentials in the database it restores.

Use the managed service's backup facilities for external PostgreSQL, and the provider's for external object storage. A completed snapshot is not a restore test.

Upgrade

Read the Norn release notes and take a backup first. Then set the release in .env:

.env
NORN_VERSION=v1.1.0
./setup.sh upgrade

The installer's upgrade is docker compose pull followed by docker compose up -d, and either form does the same thing:

docker compose pull
docker compose up -d

Compose recreates the containers whose image changed. The migrate container applies the new release's database migrations and the seed container reconciles the authorisation policy before the API, worker, and dashboard start. Both run from the same image tag as the application, which is what keeps the policy and the binary in step.

Verify the result:

docker compose ps
curl -fsS https://norn.example.com/v1/health

Then test sign-in, an issue mutation, an attachment upload and download, and an operation handled by the worker.

When release notes say the compose file itself changed, download it again before pulling, and keep your own changes in compose.override.yaml so they survive:

curl -fsSLO https://get.norn.so/compose.yaml
warning

Changing the PostgreSQL image across major versions does not upgrade its data directory. Use PostgreSQL's supported major-upgrade procedure or migrate to a new external database.

Rollback

Before returning to a previous release, determine whether the failed upgrade completed its migrations:

docker compose logs migrate

Norn does not reverse migrations. If they applied, the previous application must still be compatible with the current schema; when that is unknown, restore the pre-upgrade backup into an isolated environment and validate there instead of repeatedly changing production.

If the migrations did not apply, set NORN_VERSION back and bring the stack up again.

Restore

Restore to a separate host and hostname first:

  1. install the same NORN_VERSION with the original .env, including the encryption key
  2. start only the data services with docker compose up -d postgres valkey
  3. restore PostgreSQL and the attachment volume
  4. run docker compose up -d, which applies any newer migrations and reconciles the policy
  5. check sign-in, an issue, and an attachment download
  6. move traffic only after validation
docker compose exec -T postgres \
pg_restore --username norn --dbname norn --clean --if-exists < norn.dump

docker run --rm \
--volume norn_blob-data:/blobs \
--volume "$PWD:/backup" \
busybox:1.37.0 tar xzf /backup/norn-blobs.tar.gz -C /blobs

Use isolated copies of data services. This prevents recovery tests from sending production email, webhooks, or object-storage requests.

Inspect a running instance

docker compose ps
docker compose logs --follow api
docker compose logs migrate seed

Norn logs one JSON object per line on standard output. The API, worker, and the one-shot containers run a distroless image with no shell, so docker compose logs is the way in; there is nothing to exec into.

The job queues have their own operator surface, which runs from the same image:

docker compose run --rm --no-deps api jobs queues
docker compose run --rm --no-deps api jobs list default archived

Archived tasks are the dead-letter queue. jobs run, jobs archive, and jobs delete act on a single task by queue and identifier.

Stop and remove

docker compose down

This stops and removes the containers and keeps every volume, so the database, attachments, and certificates survive. Bringing the stack down is not data deletion.

docker compose down --volumes

This removes the volumes as well and destroys the instance's data. Run it only after a final, verified backup and an explicit data-destruction decision.