# Local / single-host deployment. # # Secrets come from .env (gitignored). Copy .env.example to .env and set a # strong POSTGRES_PASSWORD plus a matching DATABASE_URL before `docker compose # up`. For a production database, point DATABASE_URL at a TLS-enabled endpoint, # e.g. postgres://user:pass@db.example.com:5432/ctcloud?sslmode=verify-full # # The compose bridge network is private; sslmode=disable is only acceptable here # because the database is not reachable from outside the stack. services: postgres: image: postgres:17-alpine environment: POSTGRES_USER: ${POSTGRES_USER:-ctcloud} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} POSTGRES_DB: ${POSTGRES_DB:-ctcloud} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-ctcloud} -d ${POSTGRES_DB:-ctcloud}"] interval: 5s timeout: 5s retries: 12 mem_limit: 1g cpus: 2.0 pids_limit: 256 logging: driver: json-file options: { max-size: "10m", max-file: "3" } restart: unless-stopped ctcloud: build: . depends_on: postgres: condition: service_healthy # All optional tuning (DNS_SERVERS, PROBE_*, UI_USER, ...) comes from .env. # Leave DNS_SERVERS unset to use the curated + community resolver list. env_file: - .env environment: METRICS_ADDR: ":9090" # Loopback by default. Set UI_BIND=0.0.0.0 in .env to reach the UI/metrics # from another host (there is no auth unless UI_USER/UI_PASS is set). ports: - "${UI_BIND:-127.0.0.1}:9090:9090" # No application volume: the container is stateless apart from PostgreSQL. read_only: true tmpfs: - /tmp:size=16m,noexec,nosuid security_opt: - no-new-privileges:true cap_drop: - ALL mem_limit: 1g cpus: 2.0 pids_limit: 512 logging: driver: json-file options: { max-size: "10m", max-file: "3" } restart: unless-stopped volumes: pgdata: