# Thermograph production stack: the FastAPI app plus its PostgreSQL 18 database. # # docker compose up -d --build # or: make up # # POSTGRES_PASSWORD must be set at `docker compose` time — compose reads it from # the repo-root .env for local runs (copy .env.example -> .env), and in prod the # systemd unit's EnvironmentFile=/etc/thermograph.env puts it in the environment # so `docker compose up` can interpolate it. It is used BOTH to initialize the db # container and to build the app's THERMOGRAPH_DATABASE_URL below. services: db: # PostgreSQL 18 + pg_duckdb (deploy/db/Dockerfile.db, FROM pgduckdb/pgduckdb:18-…, # which IS PostgreSQL 18). Lets the DB read the app's parquet climate cache # directly, e.g. SELECT * FROM read_parquet('/parquet/cache/*.parquet'). The init # script CREATE EXTENSIONs pg_duckdb on a fresh volume. build: context: . dockerfile: deploy/db/Dockerfile.db environment: POSTGRES_USER: thermograph POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} POSTGRES_DB: thermograph volumes: # Mount the volume at the PARENT of the data dir and let the PG18 image use # its own versioned subdir (/var/lib/postgresql/18/docker). Pinning PGDATA # directly at the mountpoint trips an initdb chmod on some Docker setups; the # whole tree still persists on the named volume this way. - pgdata:/var/lib/postgresql # Read the app's live parquet cache (the same appdata volume the app writes, # mounted read-only) — query it under /parquet/cache/*.parquet. - appdata:/parquet:ro - ./deploy/db/init:/docker-entrypoint-initdb.d healthcheck: test: ["CMD-SHELL", "pg_isready -U thermograph -d thermograph"] interval: 5s timeout: 5s retries: 10 # Cap the DB at 2 CPUs. Compose v2 honors the top-level `cpus:`; the # deploy.resources block is the Swarm-style equivalent, kept for parity. cpus: 2.0 deploy: resources: limits: cpus: "2.0" restart: unless-stopped # No host port on purpose: the app reaches Postgres as db:5432 on the # compose network. Nothing outside the stack should touch the database. app: build: . depends_on: db: condition: service_healthy environment: # Built from POSTGRES_PASSWORD; this `environment` value wins over anything # in env_file, so the URL always matches the db container's password. THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph:${POSTGRES_PASSWORD}@db:5432/thermograph THERMOGRAPH_BASE: / PORT: 8137 WORKERS: 4 # One worker wins this lock and runs the subscription notifier / homepage # sweep; it lives on the appdata volume so it's shared across workers. THERMOGRAPH_SINGLETON_LOCK: /app/data/notifier.lock # Prod secrets live in /etc/thermograph.env: POSTGRES_PASSWORD, # THERMOGRAPH_AUTH_SECRET, THERMOGRAPH_VAPID_PRIVATE_KEY/_PUBLIC_KEY, # THERMOGRAPH_COOKIE_SECURE=1, mail/Discord keys, ... (see # deploy/thermograph.env.example). `required: false` so local `docker compose # up` works without that file — it reads POSTGRES_PASSWORD from repo-root .env. env_file: - path: /etc/thermograph.env required: false volumes: # Parquet cache, notifier.lock, homepage.json, vapid.json persist here. - appdata:/app/data - applogs:/app/logs cpus: 4.0 ports: # Loopback only — host Caddy terminates TLS and reverse-proxies to this. - "127.0.0.1:8137:8137" restart: unless-stopped volumes: pgdata: {} appdata: {} applogs: {}