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
      # Optional: AI features (users can also bring their own key)
      # AI_PROVIDER: claude
      # AI_PROVIDER_CLAUDE_API_KEY: "sk-ant-..."   # shared key, optional
      # AI_TRIAL_USAGE_LIMIT: 10      # per user and month, in US cents; 0 = no limit
    volumes:
      # Required: all uploaded images (avatars, backgrounds, project pictures) live in /app/data
      - ./comaney/data:/app/data

  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:
      - ./comaney/mariadb:/var/lib/mysql

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
      # AI features with shared access (users can also bring their own key); other services: see AI Providers
      AI_PROVIDER: claude
      AI_PROVIDER_CLAUDE_API_KEY: "sk-ant-..."
      AI_TRIAL_USAGE_LIMIT: 10         # per user and month, in US cents; 0 = no limit
      # Legal pages (Impressum / Datenschutzerklaerung), mounted below
      PUBLIC_PAGE_IMPRINT_MD: /pages/imprint.md
      PUBLIC_PAGE_EUDATENSCHUTZ_MD: /pages/dsgvo.md
    volumes:
      # Required: all uploaded images (avatars, backgrounds, project pictures) live in /app/data
      - /srv/comaney/data:/app/data
      # Legal pages content
      - /srv/comaney/imprint.md:/pages/imprint.md:ro
      - /srv/comaney/dsgvo.md:/pages/dsgvo.md:ro

  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:
      - /srv/comaney/mariadb:/var/lib/mysql

Both examples use Claude for the AI features. Other services (DeepSeek, a self-hosted model) and all their options: see AI Providers.

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 every uploaded file under /app/data inside the container. This path is fixed by the image; you only choose where it lives on the host (e.g. /srv/comaney/data:/app/data). It must be a persistent mount: without it, the app will refuse to start.

What is stored there:

Path Contents
/app/data/media/ppics/ User avatars (JPEG, one per user)
/app/data/media/backdrops/ Personal backgrounds (PNG, one per user, visible only to their owner)
/app/data/media/bgpics/ Project pictures (WebP, one per project)
/app/data/media/offline-buddy-ppic/ Avatars of offline contacts and offline members (JPEG)

Always mount a host directory (or a named volume) to /app/data, and include it in your backups.

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 (see Shared AI Access).

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.