Skip to content

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:

docker exec -it comaney-web-1 python manage.py set_user_password user@example.com

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:

docker exec -it comaney-web-1 python manage.py set_user_password user@example.com

Remove every second-factor method so the user can log in with their password again:

docker exec -it comaney-web-1 python manage.py remove_user_2fa user@example.com

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.

docker exec -it <container_name> python manage.py createsuperuser

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:

openssl rand -hex 50

Updating

Pull the new image and recreate the container. Migrations run automatically on startup.

docker compose pull
docker compose up -d

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.