This directory is a complete deployment of mibudge with Docker Compose, for
production, staging or integration hosts. Copy it to the host, fill in .env
and secrets/, and start it. Nothing here is used by local development (see
../README.md).
The stack pulls a pre-built image from GHCR (ghcr.io/scanner/mibudge),
published for linux/amd64 and linux/arm64, and expects PostgreSQL and Redis to be available, either as external services
connected via Docker networks or bundled into this Compose file.
deployment/production/
docker-compose.yml # app stack (web, celeryworker, celerybeat, migrate)
dot-env.example # non-secret config template — copy to .env
secrets/ # credential files (gitignored except .gitkeep)
README.md # this file
Each file holds one value with no trailing newline. Create them under
secrets/:
# Full Postgres connection URL including credentials
printf 'postgres://mibudge:YOURPASSWORD@postgres:5432/mibudge' > secrets/database_url
# Django signing key — generate with:
# python -c "import secrets; print(secrets.token_hex(50), end='')"
printf 'YOURKEY' > secrets/django_secret_key
# Fernet encryption key for sensitive fields — generate with:
# python -c "import secrets; print(secrets.token_hex(32), end='')"
printf 'YOURKEY' > secrets/salt_key
# SMTP credentials
printf 'smtp-user@example.com' > secrets/email_host_user
printf 'YOURPASSWORD' > secrets/email_host_password
printf (not echo) avoids the trailing newline that most editors append.
cp dot-env.example .env
$EDITOR .env
Set at minimum: VERSION, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS,
SITE_URL, DJANGO_ADMIN_URL, EMAIL_HOST, REDIS_URL,
CELERY_BROKER_URL, CELERY_RESULT_BACKEND.
The app stack connects to Postgres and Redis over Docker networks. Choose one of the two approaches below.
Run Postgres and Redis as separate stacks. Each stack creates a named Docker network that the app stack joins as an external network.
Postgres — using the
postgres-docker-compose
template:
# On the same host, in the postgres stack directory:
docker compose up -d
# This creates the `postgres_net` network.
Redis — example minimal stack:
# redis/docker-compose.yml
# services:
# redis:
# image: redis:7-alpine
# restart: unless-stopped
# command: redis-server --save 60 1 --loglevel warning
# networks: [redis_network]
# networks:
# redis_network:
# name: redis_network
docker compose -f redis/docker-compose.yml up -d
# This creates the `redis_network` network.
Both networks must exist before starting the app stack.
Run Postgres and Redis inside this stack. Add a secret file for the database
password, using the same password as in secrets/database_url:
printf 'YOURPASSWORD' > secrets/postgres_password
Then make these changes to docker-compose.yml. Under secrets:, add:
postgres_password:
file: ./secrets/postgres_password
Under services:, add:
postgres:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_DB: mibudge
POSTGRES_USER: mibudge
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
PGDATA: /var/lib/postgresql/data
secrets:
- postgres_password
volumes:
- postgres_data:/var/lib/postgresql/data
networks: [postgres_net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mibudge -d mibudge"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --save 60 1 --loglevel warning
volumes:
- redis_data:/data
networks: [redis_network]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
Give migrate a depends_on so it waits for both (web and the Celery
services already wait for migrate):
migrate:
<<: *base
restart: "no"
command: python /app/manage.py migrate --no-input
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
Replace the networks: section at the bottom with networks this stack owns,
and add the volumes:
networks:
postgres_net:
redis_network:
volumes:
postgres_data: {}
redis_data: {}
With bundled services, secrets/database_url is
postgres://mibudge:YOURPASSWORD@postgres:5432/mibudge, and the Redis
settings in .env stay as dot-env.example has them
(redis://redis:6379/0, /1 and /2).
docker compose pull
docker compose up -d
The migrate service runs once on startup and exits; the remaining services
start after it completes successfully.
View logs:
docker compose logs -f web
docker compose logs -f celeryworker
curl -I https://your-domain/app/login/
# Expect: HTTP/2 200
Log in, create a bank account, import a statement, and confirm the celery
worker processes the task (check docker compose logs celeryworker).
Edit .env to set the new version, then pull and restart:
# In .env: VERSION=1.2.3
docker compose pull
docker compose up -d
migrate runs automatically on every up, applying any new migrations before
the web and worker services start.
If the release you are leaving has no migrations, edit .env to the previous
version, then restart:
# In .env: VERSION=1.2.2
docker compose up -d
The migrate service only migrates forward, so a release with migrations
needs them reversed first, while it is still the deployed version:
# Still on the newer release: migrate each changed app back to its last
# migration in the release you are returning to.
docker compose run --rm migrate python /app/manage.py migrate <app> <migration>
# In .env: VERSION=1.2.2
docker compose up -d
Reversing a migration that dropped a column or deleted rows does not bring
that data back, and some migrations cannot be reversed at all; then restore
the database from a backup taken before the upgrade instead. See
docs/releasing.md.
SALT_KEY is used to derive the encryption key for sensitive database fields
(bank account numbers, etc.). To rotate it:
python -c "import secrets; print(secrets.token_hex(32), end='')"
secrets/salt_key to a comma-separated list — new key first:
printf 'NEW_KEY,OLD_KEY' > secrets/salt_key
The first value encrypts all new data; the rest decrypt existing records.
Restart the stack: docker compose up -d
Re-save all records that contain encrypted fields so they are re-encrypted with the new key (a Django management command or admin action).
secrets/salt_key
and restart again.