Skip to content

Production

Production is the Compose project tempest-prod. How the two stacks differ, and how a code change is picked up on each, is in workflow.md. The assistant worker is described in assistants.md. Backups are in restore.md.

Do not change Cloudflare, DNS, DDNS, or Nginx Proxy Manager unless asked. This page records where the public name goes. It is not an instruction to move it.

Where the public name goes

Checked on 31 October 2026 from the Nginx Proxy Manager vhost for tempest.ajphoto.co.uk and from docker ps.

https://tempest.ajphoto.co.uk is proxied to 192.168.10.20 port 8000, the published port of tempest-dev-web-1. A signup or a photo saved on that name lands in data/dev.

tempest-prod-web-1 publishes no port. It is on the npm_default network and is not the upstream of that vhost. tempest-prod-db-1 and tempest-prod-scheduler-1 were running. tempest-prod-assistant-1 was not.

The vhost sends HSTS (max-age=63072000; preload). Nginx's global client_max_body_size is 2000m. The tempest server block does not set its own body limit or proxy read timeout. Development does not treat the forwarded request as HTTPS (SECURE_PROXY_SSL_HEADER is production only). Sign-in from the public name depends on CSRF_TRUSTED_ORIGINS in .env.dev including https://tempest.ajphoto.co.uk.

A phone on the house Wi-Fi can also open http://192.168.10.20:8000 and skip the proxy. That address is plain HTTP, so the installed app cannot register its service worker there.

Docs site

The documentation site is the Compose project tempest-docs. It publishes 192.168.10.20 port 8090. How to start it is in workflow.md.

docs.tempest.ajphoto.co.uk is the name for that port. It is not a proxy host yet. When it is added, the upstream is HTTP to port 8090. The site is Markdown, so it needs none of the upload limits below.

  • Live reload uses a WebSocket on port 8090. The proxy host needs WebSockets enabled, or a saved page does not refresh in the browser.
  • The proxy should not cache this host. A cached page hides an edit.
  • The pages include restore commands, the house address, and how admin and the assistant worker run. The container does not add a password. An access list on the proxy is what keeps them off the public internet.

What the app needs from a proxy

These are the limits the upload path and the service worker are written for. They describe the app, not a change to make to the proxy.

  • /sw.js and the manifest are sent Cache-Control: no-cache (pwa/views.py). A proxy that caches /sw.js will serve an old shell to installed phones.
  • Tempest accepts a photo up to 30 MB and enforces that itself. The upload is multipart, so a proxy limit of 30m can reject a photo the app would accept. 32m is enough for a 30 MB photo plus the form boundary.
  • Photo conversion runs inside the upload request. Gunicorn's timeout is 120 seconds. A proxy read timeout of 180 seconds covers a large original on a slow mobile connection plus that conversion.

Updating production

cd /home/ajoyner/tempest
docker compose -f compose.prod.yaml build
docker compose -f compose.prod.yaml up -d

Migrations and collectstatic run when web starts. Installed phones see "Update ready", and it applies once nothing is being captured or uploaded. The public name keeps serving whichever upstream the proxy host already has. Rebuilding production does not move that name onto production.

The assistant worker is a separate container from the same image. Migrations on web do not restart it. After a change to CURSOR_API, the assistant tools, or requirements.txt:

docker compose -f compose.prod.yaml build web
docker compose -f compose.prod.yaml up -d --no-deps --force-recreate web assistant

Recreating a container from the old image drops a key or package that was only installed inside the running container. Leave this until you mean to deploy it. The dev database is migrated on its own and does not change production.

To reload environment variables for web without recreating the database:

docker compose -f compose.prod.yaml up -d --no-deps --force-recreate web

Omit --no-deps and Compose recreates the database container as well. The files in data/ remain.

Accounts on production

Public signup never grants staff or the in-app console.

docker compose -f compose.prod.yaml exec web python manage.py createsuperuser
docker compose -f compose.prod.yaml exec web python manage.py make_admin you@example.com

createsuperuser opens Django /admin/. make_admin opens /app/admin. One account can have both. make_admin you@example.com --revoke removes only the console flag, and it refuses when that person is the last active admin. See accounts-and-admin.md.

Mail uses the SMTP settings in .env.prod. The password stays in that file. After changing it, recreate web so the process reads it, then:

docker compose -f compose.prod.yaml exec web python manage.py sendtestemail you@example.com

Commands

docker compose -f compose.prod.yaml ps
docker compose -f compose.prod.yaml logs -f web
docker compose -f compose.prod.yaml logs scheduler
docker compose -f compose.prod.yaml logs assistant
docker compose -f compose.prod.yaml exec web python manage.py daily_maintenance