production docker deployment update
This commit is contained in:
1 parent
11b0ca26a5
commit
ded52d1ece
12 files changed
+616
No files matched your search
@@ -0,0 +1,210 @@
|
|||||||
|
# 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
|
||||||
@@ -26,6 +26,18 @@ City A/B/C (API+DB) --API key sync--> HQ mirror API+DB --session--> Executive FE
|
|||||||
| Site `dashboard-cpsp` | `:3001` | `:8000` |
|
| Site `dashboard-cpsp` | `:3001` | `:8000` |
|
||||||
| HQ `dashboard-cpsp-executive` | `:3002` | `:8001` |
|
| HQ `dashboard-cpsp-executive` | `:3002` | `:8001` |
|
||||||
|
|
||||||
|
## Production (Docker)
|
||||||
|
|
||||||
|
Same Compose layout as the site app. On the HQ host: UI **:80**, API debug **:18001**, Postgres **:15433**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docker/.env.example .env
|
||||||
|
# set SECRET_KEY, DB_PASSWORD, BOOTSTRAP_ADMIN_PASSWORD
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
See [DOCKER.md](DOCKER.md) for architecture, env vars, cron, and ops commands.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
**Backend**
|
**Backend**
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
name: dashboard-cpsp-executive
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: dashboard-cpsp-executive-database
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-executive}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-change_this_to_a_strong_password}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-executive}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- dashboard-cpsp-executive-network
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"pg_isready -U ${DB_USER:-executive} -d ${DB_NAME:-executive}",
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
# Host 15433 avoids clash with site dashboard-cpsp (15432).
|
||||||
|
# Mac dev: add docker-compose.override.yml from docker/compose.override.local.example for :5432.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:15433:5432"
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: dashboard-cpsp-executive-api
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.api
|
||||||
|
container_name: dashboard-cpsp-executive-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18001:8000"
|
||||||
|
environment:
|
||||||
|
DEBUG: ${DEBUG:-false}
|
||||||
|
SECRET_KEY: ${SECRET_KEY:-test-secret-key}
|
||||||
|
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,api,frontend,executive.local}
|
||||||
|
CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS:-http://localhost,http://127.0.0.1,http://executive.local}
|
||||||
|
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost,http://127.0.0.1,http://executive.local}
|
||||||
|
CORS_ALLOW_CREDENTIALS: "true"
|
||||||
|
DB_ENGINE: django.db.backends.postgresql
|
||||||
|
DB_NAME: ${DB_NAME:-executive}
|
||||||
|
DB_USER: ${DB_USER:-executive}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-change_this_to_a_strong_password}
|
||||||
|
DB_HOST: database
|
||||||
|
DB_PORT: "5432"
|
||||||
|
KARUNG_WEB_ADMIN_BASE_URL: ${KARUNG_WEB_ADMIN_BASE_URL:-http://host.docker.internal:5000}
|
||||||
|
KARUNG_WEB_ADMIN_TIMEOUT_SECONDS: ${KARUNG_WEB_ADMIN_TIMEOUT_SECONDS:-30}
|
||||||
|
KARUNG_WEB_ADMIN_SYNC_ENABLED: ${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}
|
||||||
|
DASHBOARD_PUBLISH_HOUR: ${DASHBOARD_PUBLISH_HOUR:-17}
|
||||||
|
DASHBOARD_PUBLISH_MINUTE: ${DASHBOARD_PUBLISH_MINUTE:-0}
|
||||||
|
IOT_SYNC_ENABLED: ${IOT_SYNC_ENABLED:-false}
|
||||||
|
IOT_API_BASE_URL: ${IOT_API_BASE_URL:-}
|
||||||
|
IOT_FLOCK_ID_MAP: ${IOT_FLOCK_ID_MAP:-{}}
|
||||||
|
IOT_SYNC_LOOKBACK_MINUTES: ${IOT_SYNC_LOOKBACK_MINUTES:-20}
|
||||||
|
IOT_SYNC_MAX_PAGES: ${IOT_SYNC_MAX_PAGES:-10}
|
||||||
|
CHICKEN_COUNTING_EDGE_BASE_URL: ${CHICKEN_COUNTING_EDGE_BASE_URL:-}
|
||||||
|
CHICKEN_COUNTING_EDGE_TIMEOUT_SECONDS: ${CHICKEN_COUNTING_EDGE_TIMEOUT_SECONDS:-30}
|
||||||
|
CHICKEN_COUNTING_EDGE_COUNTING_SYNC_ENABLED: ${CHICKEN_COUNTING_EDGE_COUNTING_SYNC_ENABLED:-false}
|
||||||
|
CHICKEN_COUNTING_EDGE_MORTALITY_SYNC_ENABLED: ${CHICKEN_COUNTING_EDGE_MORTALITY_SYNC_ENABLED:-false}
|
||||||
|
CHICKEN_COUNTING_EDGE_WEIGHT_SYNC_ENABLED: ${CHICKEN_COUNTING_EDGE_WEIGHT_SYNC_ENABLED:-false}
|
||||||
|
BOOTSTRAP_API_KEY: ${BOOTSTRAP_API_KEY:-}
|
||||||
|
BOOTSTRAP_ADMIN_USER: ${BOOTSTRAP_ADMIN_USER:-admin}
|
||||||
|
BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:-Pr04dm1n}
|
||||||
|
BOOTSTRAP_STAFF_USER: ${BOOTSTRAP_STAFF_USER:-}
|
||||||
|
BOOTSTRAP_STAFF_PASSWORD: ${BOOTSTRAP_STAFF_PASSWORD:-}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- api-media:/app/media
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dashboard-cpsp-executive-network
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"python",
|
||||||
|
"-c",
|
||||||
|
"import urllib.request; r=urllib.request.urlopen('http://127.0.0.1:8000/api/v1/health/'); exit(0 if r.status == 200 else 1)",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
cron:
|
||||||
|
image: dashboard-cpsp-executive-cron
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.api
|
||||||
|
container_name: dashboard-cpsp-executive-cron
|
||||||
|
restart: unless-stopped
|
||||||
|
entrypoint: ["/app/entrypoint-cron.sh"]
|
||||||
|
environment:
|
||||||
|
DEBUG: ${DEBUG:-false}
|
||||||
|
SECRET_KEY: ${SECRET_KEY:-test-secret-key}
|
||||||
|
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,api,frontend,executive.local}
|
||||||
|
DB_ENGINE: django.db.backends.postgresql
|
||||||
|
DB_NAME: ${DB_NAME:-executive}
|
||||||
|
DB_USER: ${DB_USER:-executive}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-change_this_to_a_strong_password}
|
||||||
|
DB_HOST: database
|
||||||
|
DB_PORT: "5432"
|
||||||
|
KARUNG_WEB_ADMIN_BASE_URL: ${KARUNG_WEB_ADMIN_BASE_URL:-http://host.docker.internal:5000}
|
||||||
|
KARUNG_WEB_ADMIN_TIMEOUT_SECONDS: ${KARUNG_WEB_ADMIN_TIMEOUT_SECONDS:-30}
|
||||||
|
KARUNG_WEB_ADMIN_SYNC_ENABLED: ${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}
|
||||||
|
DASHBOARD_PUBLISH_HOUR: ${DASHBOARD_PUBLISH_HOUR:-17}
|
||||||
|
DASHBOARD_PUBLISH_MINUTE: ${DASHBOARD_PUBLISH_MINUTE:-0}
|
||||||
|
IOT_SYNC_ENABLED: ${IOT_SYNC_ENABLED:-false}
|
||||||
|
IOT_API_BASE_URL: ${IOT_API_BASE_URL:-}
|
||||||
|
IOT_FLOCK_ID_MAP: ${IOT_FLOCK_ID_MAP:-{}}
|
||||||
|
IOT_SYNC_LOOKBACK_MINUTES: ${IOT_SYNC_LOOKBACK_MINUTES:-20}
|
||||||
|
IOT_SYNC_MAX_PAGES: ${IOT_SYNC_MAX_PAGES:-10}
|
||||||
|
CHICKEN_COUNTING_EDGE_BASE_URL: ${CHICKEN_COUNTING_EDGE_BASE_URL:-}
|
||||||
|
CHICKEN_COUNTING_EDGE_TIMEOUT_SECONDS: ${CHICKEN_COUNTING_EDGE_TIMEOUT_SECONDS:-30}
|
||||||
|
CHICKEN_COUNTING_EDGE_COUNTING_SYNC_ENABLED: ${CHICKEN_COUNTING_EDGE_COUNTING_SYNC_ENABLED:-false}
|
||||||
|
CHICKEN_COUNTING_EDGE_MORTALITY_SYNC_ENABLED: ${CHICKEN_COUNTING_EDGE_MORTALITY_SYNC_ENABLED:-false}
|
||||||
|
CHICKEN_COUNTING_EDGE_WEIGHT_SYNC_ENABLED: ${CHICKEN_COUNTING_EDGE_WEIGHT_SYNC_ENABLED:-false}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- api-media:/app/media
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dashboard-cpsp-executive-network
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: dashboard-cpsp-executive-frontend
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile.web
|
||||||
|
container_name: dashboard-cpsp-executive-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- dashboard-cpsp-executive-network
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dashboard-cpsp-executive-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
driver: local
|
||||||
|
api-media:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Docker Compose environment — copy to project root:
|
||||||
|
# cp docker/.env.example .env
|
||||||
|
|
||||||
|
# --- PostgreSQL ---
|
||||||
|
DB_USER=executive
|
||||||
|
DB_PASSWORD=change_this_to_a_strong_password
|
||||||
|
DB_NAME=executive
|
||||||
|
|
||||||
|
# --- Django ---
|
||||||
|
SECRET_KEY=change-me-in-production-use-a-long-random-string
|
||||||
|
DEBUG=false
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,api,frontend,executive.local
|
||||||
|
CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1,http://executive.local
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1,http://executive.local
|
||||||
|
|
||||||
|
# Production admin (bootstrap_admin — no demo data)
|
||||||
|
BOOTSTRAP_ADMIN_USER=admin
|
||||||
|
BOOTSTRAP_ADMIN_PASSWORD=Pr04dm1n
|
||||||
|
|
||||||
|
# Optional production GM (leave BOOTSTRAP_STAFF_USER empty to skip)
|
||||||
|
BOOTSTRAP_STAFF_USER=
|
||||||
|
BOOTSTRAP_STAFF_PASSWORD=
|
||||||
|
|
||||||
|
# Optional: fixed API key for city-site inbound / scripts (hashed at rest)
|
||||||
|
BOOTSTRAP_API_KEY=
|
||||||
|
|
||||||
|
# City-edge syncs OFF at HQ — data should arrive via city-site mirror sync.
|
||||||
|
# Enable only if this box also pulls directly from edge services.
|
||||||
|
KARUNG_WEB_ADMIN_SYNC_ENABLED=false
|
||||||
|
IOT_SYNC_ENABLED=false
|
||||||
|
CHICKEN_COUNTING_EDGE_COUNTING_SYNC_ENABLED=false
|
||||||
|
CHICKEN_COUNTING_EDGE_MORTALITY_SYNC_ENABLED=false
|
||||||
|
CHICKEN_COUNTING_EDGE_WEIGHT_SYNC_ENABLED=false
|
||||||
|
|
||||||
|
# Dashboard publish cutoff (WIB): day D visible from 17:00 on day D
|
||||||
|
DASHBOARD_PUBLISH_HOUR=17
|
||||||
|
DASHBOARD_PUBLISH_MINUTE=0
|
||||||
|
|
||||||
|
# Optional edge endpoints (unused while sync flags are false)
|
||||||
|
KARUNG_WEB_ADMIN_BASE_URL=http://host.docker.internal:5000
|
||||||
|
KARUNG_WEB_ADMIN_TIMEOUT_SECONDS=30
|
||||||
|
IOT_API_BASE_URL=
|
||||||
|
IOT_SYNC_LOOKBACK_MINUTES=20
|
||||||
|
IOT_SYNC_MAX_PAGES=10
|
||||||
|
IOT_FLOCK_ID_MAP={}
|
||||||
|
CHICKEN_COUNTING_EDGE_BASE_URL=
|
||||||
|
CHICKEN_COUNTING_EDGE_TIMEOUT_SECONDS=30
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Backend — Django + Gunicorn (+ cron in sibling container)
|
||||||
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libjpeg62-turbo-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libpq-dev \
|
||||||
|
cron \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY backend/ .
|
||||||
|
COPY docker/entrypoint-api.sh docker/entrypoint-cron.sh docker/karung_sync_loop.sh /app/
|
||||||
|
COPY docker/crontab /etc/cron.d/executive
|
||||||
|
|
||||||
|
RUN chmod +x /app/entrypoint-api.sh /app/entrypoint-cron.sh /app/karung_sync_loop.sh \
|
||||||
|
&& mkdir -p /app/data /app/media /app/staticfiles \
|
||||||
|
&& chmod 0644 /etc/cron.d/executive \
|
||||||
|
&& crontab /etc/cron.d/executive
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint-api.sh"]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Frontend — multi-stage build (Vite + Nginx)
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV VITE_API_BASE=/api/v1
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD wget --quiet --tries=1 --spider http://localhost:80/health || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# On your Mac only — copy to docker-compose.override.yml (gitignored, not pushed):
|
||||||
|
# cp docker/compose.override.local.example docker-compose.override.yml
|
||||||
|
#
|
||||||
|
# Compose merges with docker-compose.yml: you keep the server’s 15433 mapping and add 5432
|
||||||
|
# for local DBeaver, restore scripts, or host tools. If 5432 is busy, use 5433:5432.
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5432:5432"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
SHELL=/bin/sh
|
||||||
|
PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
# NOTE: docker/entrypoint-cron.sh regenerates this file from DASHBOARD_PUBLISH_HOUR/MINUTE
|
||||||
|
# at container start. Edge syncs no-op when *_SYNC_ENABLED=false (HQ default).
|
||||||
|
|
||||||
|
*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_iot_from_api >> /var/log/cron.log 2>&1
|
||||||
|
*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_chicken_counting_from_edge >> /var/log/cron.log 2>&1
|
||||||
|
|
||||||
|
0 17 * * * root cd /app && /usr/local/bin/python manage.py sync_karung_from_web_admin >> /var/log/cron.log 2>&1 && /usr/local/bin/python manage.py recompute_kpi_rollups >> /var/log/cron.log 2>&1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
cd /app
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
python manage.py bootstrap_admin
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
exec gunicorn config.wsgi:application -c config/gunicorn.py
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
cd /app
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
printenv | grep -E '^(PATH|DJANGO_|SECRET_|DB_|KARUNG_|IOT_|CHICKEN_COUNTING_|DASHBOARD_|DEBUG|ALLOWED_|BOOTSTRAP_|API_|CORS_|CSRF_|SESSION_)' \
|
||||||
|
> /etc/environment || true
|
||||||
|
|
||||||
|
# Render publish-time cron from DASHBOARD_PUBLISH_* (default 17:00).
|
||||||
|
# Edge syncs are OFF by default at HQ (mirror via city-sites); commands no-op when disabled.
|
||||||
|
PUBLISH_HOUR="${DASHBOARD_PUBLISH_HOUR:-17}"
|
||||||
|
PUBLISH_MINUTE="${DASHBOARD_PUBLISH_MINUTE:-0}"
|
||||||
|
CRON_FILE=/etc/cron.d/executive
|
||||||
|
{
|
||||||
|
echo "SHELL=/bin/sh"
|
||||||
|
echo "PATH=/usr/local/bin:/usr/bin:/bin"
|
||||||
|
echo ""
|
||||||
|
echo "# Optional edge syncs (skipped when *_SYNC_ENABLED=false)."
|
||||||
|
echo "*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_iot_from_api >> /var/log/cron.log 2>&1"
|
||||||
|
echo "*/10 * * * * root cd /app && /usr/local/bin/python manage.py sync_chicken_counting_from_edge >> /var/log/cron.log 2>&1"
|
||||||
|
echo ""
|
||||||
|
echo "# Daily publish — optional karung sync then KPI rollups at configured cutoff."
|
||||||
|
echo "${PUBLISH_MINUTE} ${PUBLISH_HOUR} * * * root cd /app && /usr/local/bin/python manage.py sync_karung_from_web_admin >> /var/log/cron.log 2>&1 && /usr/local/bin/python manage.py recompute_kpi_rollups >> /var/log/cron.log 2>&1"
|
||||||
|
} > "$CRON_FILE"
|
||||||
|
chmod 0644 "$CRON_FILE"
|
||||||
|
crontab "$CRON_FILE"
|
||||||
|
|
||||||
|
if [ "${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}" = "true" ] || [ "${KARUNG_WEB_ADMIN_SYNC_ENABLED:-false}" = "True" ]; then
|
||||||
|
/app/karung_sync_loop.sh &
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec cron -f
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Sub-minute karung sync — cron cannot schedule under 1 minute.
|
||||||
|
# Only started when KARUNG_WEB_ADMIN_SYNC_ENABLED=true (off by default at HQ).
|
||||||
|
set -eu
|
||||||
|
cd /app
|
||||||
|
while true; do
|
||||||
|
/usr/local/bin/python manage.py sync_karung_from_web_admin >> /var/log/cron.log 2>&1 || true
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
||||||
|
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 1800s;
|
||||||
|
proxy_connect_timeout 1800s;
|
||||||
|
proxy_send_timeout 1800s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /media/ {
|
||||||
|
proxy_pass http://api:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user