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 holdsNORN_SECURITY_ENCRYPTION_KEYand the service passwordscompose.yamlandcompose.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:
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
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:
- install the same
NORN_VERSIONwith the original.env, including the encryption key - start only the data services with
docker compose up -d postgres valkey - restore PostgreSQL and the attachment volume
- run
docker compose up -d, which applies any newer migrations and reconciles the policy - check sign-in, an issue, and an attachment download
- 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.