services: db: image: timescale/timescaledb:2.31.0-pg16@sha256:322ae364eca3321cf2a48f1ef31213b87843413b3cf8feeebf54e999a26bb731 container_name: pulsebase-db restart: unless-stopped environment: POSTGRES_DB: garmin POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} ports: - "127.0.0.1:5433:5432" volumes: - timescale-data:/var/lib/postgresql/data networks: - internal labels: - "monitoring=true" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d garmin"] interval: 10s timeout: 5s retries: 5 deploy: resources: limits: memory: 2g cpus: '1.00' logging: driver: json-file options: max-size: "10m" max-file: "3" flyway: image: flyway/flyway:13@sha256:4c535796a4a08daf542ac266b4f218e01e35051a8103de876536dfcdd1808471 container_name: pulsebase-flyway command: migrate volumes: - ./db/migrations:/flyway/sql environment: - FLYWAY_URL=jdbc:postgresql://db:5432/garmin - FLYWAY_USER=${DB_USER} - FLYWAY_PASSWORD=${DB_PASSWORD} - FLYWAY_CONNECT_RETRIES=10 - FLYWAY_MIXED=true - FLYWAY_PLACEHOLDERS_DB_APP_USER=${DB_APP_USER} - FLYWAY_PLACEHOLDERS_DB_APP_PASSWORD=${DB_APP_PASSWORD} - FLYWAY_PLACEHOLDERS_DB_SYNC_USER=${DB_SYNC_USER} - FLYWAY_PLACEHOLDERS_DB_SYNC_PASSWORD=${DB_SYNC_PASSWORD} - FLYWAY_PLACEHOLDERS_DB_ML_USER=${DB_ML_USER} - FLYWAY_PLACEHOLDERS_DB_ML_PASSWORD=${DB_ML_PASSWORD} depends_on: db: condition: service_healthy networks: - internal deploy: resources: limits: memory: 256m cpus: '0.25' logging: driver: json-file options: max-size: "5m" max-file: "2" api: build: context: . dockerfile: api/Dockerfile container_name: pulsebase-api restart: unless-stopped stop_grace_period: 45s init: true security_opt: - "no-new-privileges:true" labels: - "monitoring=true" env_file: - env/.env.app - env/.env.api environment: DB_HOST: db DB_PORT: 5432 DB_NAME: garmin depends_on: flyway: condition: service_completed_successfully networks: - internal - gateway-garmin healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/ready"] interval: 30s timeout: 5s retries: 3 start_period: 30s deploy: resources: limits: memory: 512m cpus: '0.50' logging: driver: json-file options: max-size: "10m" max-file: "3" sync-service: build: context: . dockerfile: sync-service/Dockerfile container_name: pulsebase-sync restart: unless-stopped stop_grace_period: 120s init: true security_opt: - "no-new-privileges:true" labels: - "monitoring=true" depends_on: flyway: condition: service_completed_successfully env_file: - env/.env.app - env/.env.sync environment: DB_HOST: db DB_PORT: 5432 DB_NAME: garmin networks: - internal healthcheck: test: ["CMD-SHELL", "test -f /tmp/sync_alive && python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health')\""] interval: 30s timeout: 5s start_period: 120s retries: 3 deploy: resources: limits: memory: 256m cpus: '0.25' logging: driver: json-file options: max-size: "10m" max-file: "3" ml-service: build: ./ml-service container_name: pulsebase-ml restart: unless-stopped stop_grace_period: 120s init: true security_opt: - "no-new-privileges:true" labels: - "monitoring=true" depends_on: flyway: condition: service_completed_successfully env_file: - env/.env.app - env/.env.ml environment: MODEL_DIR: /app/models DB_HOST: db DB_PORT: 5432 DB_NAME: garmin volumes: - ml-models:/app/models networks: - internal healthcheck: test: ["CMD-SHELL", "test -f /tmp/ml_alive && python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health')\""] interval: 30s timeout: 5s start_period: 60s retries: 3 deploy: resources: limits: memory: 1g cpus: '1.00' logging: driver: json-file options: max-size: "10m" max-file: "3" backup: build: ./backup container_name: pulsebase-backup restart: unless-stopped labels: - "monitoring=true" # DB admin creds come from env/.env (pg_dump needs full read); backup config # from env/.env.backup (optional — stack still starts if absent, the run just # fails its preflight on a missing AGE_RECIPIENT). env_file: - path: env/.env.backup required: false environment: PGHOST: db PGPORT: 5432 PGUSER: ${DB_USER} PGPASSWORD: ${DB_PASSWORD} PGDATABASE: garmin depends_on: db: condition: service_healthy volumes: - backups:/backups networks: - internal deploy: resources: limits: memory: 256m cpus: '0.25' logging: driver: json-file options: max-size: "10m" max-file: "3" volumes: timescale-data: ml-models: backups: networks: internal: driver: bridge gateway-garmin: external: true name: gateway-garmin