Deployment¶
Comaney is distributed as a Docker image and requires a MariaDB database. A docker-compose.yml is the recommended way to run it.
Minimal docker-compose.yml¶
services:
web:
image: leonetienne/comaney:latest
restart: unless-stopped
ports:
- "80:8000"
depends_on:
- mariadb
environment:
# Generate with:
# python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
DJANGO_SECRET_KEY: "change-me-to-a-long-random-string"
DB_HOST: mariadb
DB_PORT: 3306
DB_NAME: comaney
DB_USER: comaney
DB_PASSWORD: "change-me"
SITE_URL: "http://localhost:80"
ALLOWED_HOSTS: "localhost"
CSRF_TRUSTED_ORIGINS: "http://localhost"
# Allow new users to register (disable after initial setup)
ENABLE_REGISTRATION: "TRUE"
# No SMTP server? Disable emailing for a quick start.
# Note: disabling email also disables email verification on registration.
DISABLE_EMAILING: "TRUE"
GUNICORN_WORKERS: 1
volumes:
- comaney_data:/app/data # persistent user data (profile pictures, etc.)
mariadb:
image: mariadb:lts
restart: unless-stopped
environment:
MARIADB_DATABASE: comaney
MARIADB_USER: comaney
MARIADB_PASSWORD: "change-me"
MARIADB_ROOT_PASSWORD: "change-root-too"
volumes:
- mariadb_data:/var/lib/mysql
volumes:
mariadb_data:
comaney_data:
Production docker-compose.yml (with SMTP and HTTPS)¶
services:
web:
image: leonetienne/comaney:latest
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000" # bind to localhost; reverse proxy handles TLS
depends_on:
- mariadb
environment:
DJANGO_SECRET_KEY: "your-secret-key-here"
DB_HOST: mariadb
DB_PORT: 3306
DB_NAME: comaney
DB_USER: comaney
DB_PASSWORD: "strong-db-password"
SITE_URL: "https://budget.example.com"
ALLOWED_HOSTS: "budget.example.com"
CSRF_TRUSTED_ORIGINS: "https://budget.example.com"
ENABLE_REGISTRATION: "FALSE"
EMAIL_HOST: "smtp.example.com"
EMAIL_PORT: 587
EMAIL_USE_TLS: "TRUE"
EMAIL_HOST_USER: "noreply@example.com"
EMAIL_HOST_PASSWORD: "smtp-password"
DEFAULT_FROM_EMAIL: "Comaney <noreply@example.com>"
ADMIN_NOTIFICATION_EMAIL: "admin@example.com"
GUNICORN_WORKERS: 2
volumes:
- comaney_data:/app/data # persistent user data (profile pictures, AI trial flag, etc.)
mariadb:
image: mariadb:lts
restart: unless-stopped
environment:
MARIADB_DATABASE: comaney
MARIADB_USER: comaney
MARIADB_PASSWORD: "strong-db-password"
MARIADB_ROOT_PASSWORD: "strong-root-password"
volumes:
- mariadb_data:/var/lib/mysql
volumes:
mariadb_data:
comaney_data:
Transport security behind a reverse proxy¶
Comaney is built to run behind a reverse proxy (nginx, Traefik, Caddy, etc.) that terminates HTTPS and forwards plain HTTP to the app on port 8000. The app speaks HTTP and never tries to enforce HTTPS on its own: it does not redirect HTTP to HTTPS, and it does not send HSTS. Those two jobs belong to your proxy, which is the only part of the stack that actually sees the TLS connection.
This means you do not set any HTTPS-related environment variables on the app. Enabling app-side HTTPS redirection while sitting behind a proxy that already terminates TLS creates an endless redirect loop (the browser and the app bounce the same request back and forth), which is why the app deliberately leaves all of that to the proxy.
When DEBUG is off, the one thing the app still does for you is mark its login (session) and CSRF cookies as Secure, so browsers only ever send them over the encrypted connection to your proxy. Nothing to configure for that; it is automatic.
Configure HTTPS redirection and HSTS on the proxy itself. For example, in nginx:
server {
listen 80;
server_name budget.example.com;
return 301 https://$host$request_uri; # HTTP -> HTTPS redirect lives here
}
server {
listen 443 ssl;
server_name budget.example.com;
# ... your TLS certificate directives ...
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # HSTS lives here
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Because ALLOWED_HOSTS no longer falls back to a wildcard in production, you must set it to your real domain(s) or the app will reject every request.
The /data volume¶
Comaney stores user-generated files under /app/data inside the container. This directory must be a persistent mounted volume — if it is missing, the application will refuse to start and display a configuration error page instead.
What is stored there:
| Path | Contents |
|---|---|
/app/data/media/ppics/ |
User profile pictures (resized JPEG, one per user) |
/app/data/ai_trial_disabled.flag |
AI trial deactivation flag (written by the admin panel) |
Always mount a named volume (or a host path) to /app/data. Do not use a tmpfs mount, as data would be lost on container restart.
First run¶
When the container starts for the first time, the entrypoint runs Django migrations automatically. No manual migration step is needed.
The web service is available on port 8000 inside the container (mapped to 80 or wherever you choose on the host).
Creating user accounts¶
With registration enabled (ENABLE_REGISTRATION=TRUE) users can sign up through the web UI. You can also create accounts directly from the command line, bypassing the registration flow. Accounts created this way are immediately active and confirmed.
# Prompted interactively (recommended):
docker exec -it comaney-web-1 python manage.py create_user user@example.com
# Or with password provided directly:
docker exec -it comaney-web-1 python manage.py create_user user@example.com -p "SecurePassword123"
To change a user's password later:
Account recovery¶
If a user forgets their password or loses access to every second-factor method (authenticator app, security keys), you can recover their account from the command line without any downtime.
Reset a forgotten password:
Remove every second-factor method so the user can log in with their password again:
See Console Commands for the full reference.
Creating a superuser¶
A superuser account is purely optional but required to access the Django admin at /admin/ and to re-enable the AI trial at /admin/ai-trial/. The AI trial deactivates itself if your trial API key runs out of funds.
Replace <container_name> with the name shown by docker compose ps (typically comaney-web-1).
Generating a secret key¶
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
Or with openssl:
Updating¶
Pull the new image and recreate the container. Migrations run automatically on startup.
Worker count¶
The GUNICORN_WORKERS variable controls how many Gunicorn worker processes are started. The default is 1. For a small private instance, 1–2 workers is sufficient. Each worker holds a database connection, so set this in proportion to your MariaDB max_connections.