Without DATABASE_HOST the app uses SQLite, which defaulted to
/app/db.sqlite3: inside the image, not writable by uid 10001, and
unusable with a read-only root filesystem. The image now has a /data
directory owned by the app user, declared as a volume, and SQLITE_PATH
defaults to /data/db.sqlite3. A fresh named volume mounted there is
writable. Checked with docker run --read-only: migrations, the app, and
a restart all keep the database.
collectstatic only needs the five required settings now, so the build
passes placeholders for that one command instead of bind-mounting
dev/env.dev. The build context no longer needs dev/ at all. Also point
the header comment at migrate_locked and keep compose.yaml out of the
build context.
The old Dockerfile copied the abandoned v2 app and a requirements file
that no longer existed, and needed build tools and a libldap workaround
for python-ldap.
The new image:
- python:3.15-slim (Debian trixie) with security updates applied; no
compiler and no sshd
- runs gunicorn (gthread workers; GUNICORN_* overrides) as uid 10001
- works with a read-only root filesystem and all capabilities dropped
- runs collectstatic at build time; the dev env file is mounted for that
step only and is not stored in the image
- does not run migrations on start; run manage.py migrate once per
release (an init container in Kubernetes)
Checked against the dev LDAP and PostgreSQL containers: migrate, both
health probes, static files and a full login through gunicorn work.