Skip to content

Docker

The repository ships with a production-style docker-compose.yml that brings up everything you need: Postgres, Redis, the Django backend, a Celery worker, and the SvelteKit frontend.

Minimum requirements

  • CPU: 2 vCPU (4 for production)
  • RAM: 2 GB (4 GB recommended)
  • Disk: 10 GB
  • OS: Anything that runs Docker: Linux, macOS, Windows with WSL2

Topology

┌────────────┐   ┌────────────┐   ┌─────────┐
│  frontend  │ → │  backend   │ → │   db    │
│  (sveltekit│   │  (django)  │   │ (psql)  │
│   :5173)   │   │   :8000    │   └─────────┘
└────────────┘   └────────────┘        │
                       │               │
                       ↓               │
                 ┌─────────┐           │
                 │  redis  │ ← worker ─┘
                 └─────────┘   (celery)

The frontend talks to the backend over HTTP. The backend uses Redis as the Celery broker and result store. Both backend and worker connect to Postgres through a non-superuser role so row-level security is enforced.

Running it

git clone https://github.com/MicroPyramid/Django-CRM.git
cd Django-CRM
# .env.docker is committed with dev defaults. Edit it, or add a gitignored
# .env.docker.local alongside it: values there win.
$EDITOR .env.docker
docker compose up --build -d

docker compose exec backend python manage.py migrate
docker compose exec backend python manage.py seed_data    # optional

The frontend is now at http://localhost:5173, the API at http://localhost:8000/api/.

Production deployment

For production, run the stack behind a reverse proxy (Caddy, nginx, Traefik) that terminates TLS, and put the backend in DEBUG=False mode.

Key changes from the dev compose file:

  • Pin image tags rather than building from source.
  • Mount /app/media and the Postgres data dir on persistent volumes.
  • Set ALLOWED_HOSTS and CORS_ALLOWED_ORIGINS to your real domain.
  • Rotate DJANGO_SECRET_KEY and JWT_SIGNING_KEY to fresh values.
  • Run python manage.py manage_rls --status to confirm RLS policies are present.
  • Redact public-link tokens from the proxy's access log (below).

Public-link tokens in access logs

The task calendar feed, the invoice and estimate portal links and the satisfaction survey link carry their credential in the URL path: whoever holds the URL can open it without signing in. From django-crm 1.13.0 the backend writes [Filtered] in place of the token in its own logs, including the uvicorn and gunicorn access logs. Your reverse proxy logs every path too, and the backend cannot reach that log. With nginx, add this to the http {} context:

map $request_uri $bottlecrm_log_uri {
    "~^((?:/api/public/(?:calendar|csat|invoice|estimate)|/portal/(?:invoice|estimate)|/csat)/)[^/?]+(.*)$" "$1[Filtered]$2";
    default $request_uri;
}

map $http_referer $bottlecrm_log_referer {
    "~^((?:https?://[^/]+)?(?:/api/public/(?:calendar|csat|invoice|estimate)|/portal/(?:invoice|estimate)|/csat)/)[^/?]+(.*)$" "$1[Filtered]$2";
    default $http_referer;
}

log_format bottlecrm_redacted '$remote_addr - $remote_user [$time_local] '
                              '"$request_method $bottlecrm_log_uri $server_protocol" '
                              '$status $body_bytes_sent "$bottlecrm_log_referer" "$http_user_agent"';

Then use it in the server {} blocks for both the API and the frontend, keeping each block's log path, for example access_log /var/log/nginx/access.log bottlecrm_redacted;, and run nginx -t before reloading. The Referer needs the same treatment because the portal pages are same-origin, so every asset they load carries the page URL. nginx's error_log cannot be reformatted, so an upstream error on one of these paths still records it there, and log lines written before the change still hold working tokens.

Backups

There are two things to back up:

  1. The Postgres database: pg_dump --format=custom is enough. Take it nightly and offsite-replicate it.
  2. Uploaded media: anything under the backend's media/ directory (attachments on cases, contact avatars, etc).

Restore is a vanilla pg_restore plus copying the media directory back.