# Docker Deployment Guide Deploy **dashboard-cpsp-executive** (HQ) with Docker Compose. Same layout as site `dashboard-cpsp` (Postgres + API + Nginx frontend + cron), with HQ ports and city-edge syncs **off** by default. ## Architecture ``` ┌──────────────────────────────────────────────────────────────┐ │ HQ Host Server │ │ │ │ ┌─────────────┐ /api/* ┌──────────────┐ │ │ │ frontend │────────────►│ api │ │ │ │ (Nginx) │ │ (Django) │ │ │ │ port 80 │ │ port 8000 │ │ │ └─────────────┘ └──────┬───────┘ │ │ │ │ │ ┌─────────────┐ ▼ │ │ │ cron │────────────► ┌──────────────┐ │ │ │ (same image)│ │ database │ │ │ └─────────────┘ │ (PostgreSQL) │ │ │ │ port 15433* │ │ │ └──────────────┘ │ └──────────────────────────────────────────────────────────────┘ * 15433 on host → 5432 in container (SSH tunnel / DBeaver) Host API debug port: 18001 → 8000 (site app uses 18000 / 15432) ``` | Container | Image / build | Host port | Role | | ---------- | ------------------ | ---------------- | ----------------------------- | | frontend | `docker/Dockerfile.web` | 80 → 80 | React SPA + Nginx API proxy | | api | `docker/Dockerfile.api` | 18001 → 8000 | Gunicorn, migrations, static | | cron | same as api | — | KPI rollups (+ optional edge syncs) | | database | postgres:16-alpine | 15433 → 5432 | PostgreSQL | ## Prerequisites - Docker 20.10+ - Docker Compose v2+ - Ports available: **80** (UI), **18001** (direct API), **15433** (Postgres on localhost) ```bash docker --version docker compose version ``` ## Quick start ```bash cd dashboard-cpsp-executive cp docker/.env.example .env # Edit .env — set SECRET_KEY and DB_PASSWORD docker compose up -d --build ``` API entrypoint runs `migrate` + `bootstrap_admin` on every start (needs `BOOTSTRAP_ADMIN_PASSWORD` in `.env`). **Production** (empty database, no demo data): log in with the bootstrap admin, then create users/sites via the UI. Do **not** run `seed_demo` on production. **Dev / demo** (sample data): ```bash docker compose exec api python manage.py seed_demo ``` ### Verify ```bash docker compose ps docker compose logs -f curl http://localhost/health curl http://localhost/api/v1/health/ ``` Open **http://localhost** in a browser. ## Configuration files (`docker/`) | File | Purpose | | ---- | ------- | | `Dockerfile.web` | Frontend image (Vite + Nginx) | | `Dockerfile.api` | Backend image (Django + Gunicorn) | | `nginx.conf` | Nginx proxy config for frontend | | `entrypoint-api.sh` | API container startup | | `entrypoint-cron.sh` | Cron container startup | | `crontab` | Fallback schedule; runtime regenerated from `DASHBOARD_PUBLISH_*` | | `.env.example` | Compose env template → copy to project root `.env` | | `compose.override.local.example` | Optional local Postgres port override | ## Configuration Compose reads variables from a root `.env` file. Template: `docker/.env.example`. Important production values: | Variable | Purpose | | -------- | ------- | | `SECRET_KEY` | Django secret — use a long random string | | `DB_PASSWORD` | PostgreSQL password | | `CSRF_TRUSTED_ORIGINS` | Must include your public UI origin (e.g. `https://executive.example.com`) | | `CORS_ALLOWED_ORIGINS` | Same as above if the SPA is on a different origin | | `BOOTSTRAP_ADMIN_USER` | Superuser username for `bootstrap_admin` (default `admin`) | | `BOOTSTRAP_ADMIN_PASSWORD` | Superuser password (required) | | `BOOTSTRAP_STAFF_USER` | Optional GM username; leave empty to skip | | `BOOTSTRAP_STAFF_PASSWORD` | GM password (required when `BOOTSTRAP_STAFF_USER` is set) | | `BOOTSTRAP_API_KEY` | Optional fixed API key for city inbound / scripts (hashed at rest) | City-edge sync flags default to **false** at HQ. Enable only if this host also pulls directly from IoT / karung / chicken-counting edge APIs. ## Management ```bash # Start / stop docker compose start docker compose stop docker compose restart # Logs docker compose logs -f api docker compose logs -f frontend docker compose logs -f cron # Django shell docker compose exec api python manage.py shell # Database (psql) docker compose exec database psql -U executive -d executive # From host (port 15433) psql -h localhost -p 15433 -U executive -d executive ``` ### Update after code changes ```bash git pull docker compose down docker compose up -d --build ``` Rolling update (less downtime): ```bash docker compose build docker compose up -d --no-deps --build api docker compose up -d --no-deps --build cron docker compose up -d --no-deps --build frontend ``` ### Local DBeaver on Mac ```bash cp docker/compose.override.local.example docker-compose.override.yml ``` Adds `127.0.0.1:5432:5432` while keeping the server’s `15433` mapping. ## Cron jobs The `cron` service runs: - **Every 10 min** — `sync_iot_from_api` / `sync_chicken_counting_from_edge` (no-op while sync flags are false) - **At `DASHBOARD_PUBLISH_HOUR`:`DASHBOARD_PUBLISH_MINUTE` daily** — optional karung sync then `recompute_kpi_rollups` Karung 30s loop starts only when `KARUNG_WEB_ADMIN_SYNC_ENABLED=true`. Logs: `docker compose exec cron tail -f /var/log/cron.log` ## Troubleshooting **API unhealthy** ```bash docker compose logs api docker compose exec api python manage.py migrate --plan ``` **Frontend 502 on /api** ```bash docker compose ps curl http://127.0.0.1:18001/api/v1/health/ ``` **Database connection errors** Check Postgres is healthy and credentials in `.env` match `docker-compose.yml` defaults. **Port conflicts** Change mappings in `docker-compose.yml`, e.g. `"8080:80"` for frontend. Site app already uses 80 / 18000 / 15432 on city hosts. **Reset database** (destructive) ```bash docker compose down -v docker compose up -d --build ``` ## Security notes - Do not commit `.env` with real secrets. - Use strong `SECRET_KEY` and `DB_PASSWORD` in production. - Restrict database port `15433` to localhost (already bound to `127.0.0.1`). - Put HTTPS in front of port 80 (reverse proxy + Let's Encrypt) for public deployment. --- Docker deployment guide — dashboard-cpsp-executive