Files
Andy Dixon ada0b18b3f deploy: production deployment for noema.dixon.cx
The systemd unit, nginx vhost, build-and-restart script and operator notes
for running Noema as a Docker container behind nginx against the host
PostgreSQL cluster. Secrets stay in /etc/noema/noema.env, outside the repo.
2026-10-10 23:30:35 +01:00
..

Production deployment (noema.dixon.cx)

Noema runs as a Docker container supervised by systemd, talks to the host PostgreSQL 16 cluster over the docker bridge, and sits behind nginx. Access control is Noema's own sign-in page. Cloudflare proxies the hostname (wildcard *.dixon.cx).

Cloudflare -> nginx :443 (TLS) -> 127.0.0.1:29630 -> container :8080 -> postgres 172.17.0.1:5432

Deploying a new version

git pull
deploy/prod/build.sh

build.sh builds noema:<git describe> and noema:latest, then restarts the noema service. Migrations are applied automatically when the daemon starts. Roll back with docker tag noema:<older-version> noema:latest && sudo systemctl restart noema (docker images noema lists the versions you have).

Where things live

What Where
systemd unit /etc/systemd/system/noema.service (source: deploy/prod/noema.service)
Environment and secrets /etc/noema/noema.env (root, 0600): DB URL, master key, bootstrap admin
nginx vhost /etc/nginx/sites-enabled/noema.dixon.cx.conf (source: deploy/prod/noema.dixon.cx.conf)
TLS certificate /etc/letsencrypt/live/noema.dixon.cx (certbot, nginx authenticator, auto-renews)
Research folder /srv/noema/research mounted at /research (owned by uid 10001)
Database noema database, noema role on the host cluster; vector extension created by postgres
Logs journalctl -u noema -f (JSON); nginx /var/log/nginx/noema_*.log

The daemon ignores proxy headers, so it sees every visitor as the docker gateway address: the login rate limit (20/min) is shared by everyone, and the per-account lockout is what actually stops password guessing. Use a strong admin password and enable TOTP on the account page.

Back up NOEMA_MASTER_KEY. It encrypts provider API keys stored in the database; losing it means re-entering every key.

Common tasks

# noemactl against the running instance
docker exec -it noema noemactl --help

# psql as the app (from the host use the postgres superuser instead)
sudo -u postgres psql noema

Rebuilding from scratch

Applies to a fresh host; everything below was done once on this one.

sudo -u postgres psql -c "CREATE ROLE noema LOGIN PASSWORD '...'" -c "CREATE DATABASE noema OWNER noema"
sudo -u postgres psql -d noema -c "CREATE EXTENSION vector"   # needs superuser; migration 0005 expects it
# pg_hba.conf must allow 172.17.0.0/16 and postgresql.conf must listen on 172.17.0.1

sudo install -d -m 700 /etc/noema && sudo install -d -o 10001 -g 10001 /srv/noema/research
sudoedit /etc/noema/noema.env   # keys as in .env.example, plus:
#   NOEMA_ENV=production  NOEMA_HTTP_ADDR=0.0.0.0:8080  NOEMA_RESEARCH_DIR=/research
#   NOEMA_DATABASE_URL=postgres://noema:PASS@172.17.0.1:5432/noema?sslmode=disable
#   NOEMA_MASTER_KEY=$(openssl rand -base64 32)  NOEMA_BOOTSTRAP_ADMIN_PASSWORD=...

sudo cp deploy/prod/noema.service /etc/systemd/system/ && sudo systemctl daemon-reload
deploy/prod/build.sh && sudo systemctl enable noema

sudo certbot certonly --nginx -d noema.dixon.cx --key-type ecdsa
sudo cp deploy/prod/noema.dixon.cx.conf /etc/nginx/sites-enabled/ && sudo nginx -t && sudo systemctl reload nginx