name: cyclone

# Two-container production stack for Cyclone.
#
#   backend   FastAPI on python:3.11-slim-bookworm, internal port 8000.
#   frontend  nginx:1.27-alpine serving the built React SPA + reverse-
#             proxying /api/* to the backend, host port 8080.
#
# Tag strategy:
#   - Images are tagged semver from package.json / pyproject.toml.
#   - Two aliases per image: `:latest` (set automatically by `docker
#     compose build`) and `:stable` (manually promoted after a release
#     has been running in prod for >=1 week).
#   - Compose defaults to `:stable` so a fresh `docker compose build`
#     never auto-rolls a new release.
#   - Override with `TAG=0.0.9 docker compose up -d` to roll back.

services:
  backend:
    image: cyclone-backend:${TAG:-stable}
    build:
      context: ./backend
    restart: unless-stopped
    environment:
      # SQLite path lives on the named `cyclone_db` volume.
      CYCLONE_DB_URL: "sqlite:////var/lib/cyclone/db/cyclone.db"
      # Wire the existing BackupService (SP17) to autostart on container boot.
      CYCLONE_BACKUP_AUTOSTART: "1"
      CYCLONE_BACKUP_INTERVAL_HOURS: "24"
      CYCLONE_BACKUP_RETENTION_DAYS: "14"
      # Logging — JSON to stdout (collected by `docker logs`) and to the
      # bind-mounted file (collected by host-side logrotate).
      CYCLONE_LOG_LEVEL: "INFO"
      CYCLONE_LOG_FILE: "/var/log/cyclone/cyclone.log"
      CYCLONE_LOG_JSON: "1"
      # Cookie signing key defaults to the SQLCipher key. Override in
      # production if you want to rotate them independently.
      CYCLONE_SECRET_KEY_FILE: "/run/secrets/cyclone_db_key"
      CYCLONE_COOKIE_SECURE: "1"
      # First-admin bootstrap — populated by scripts/cyclone-init.sh.
      # The `_FILE` variants are the standard Docker-secret pattern:
      # the env var points at the mounted secret file rather than
      # embedding the secret in the compose file.
      CYCLONE_ADMIN_USERNAME_FILE: "/run/secrets/cyclone_admin_username"
      CYCLONE_ADMIN_PASSWORD_FILE: "/run/secrets/cyclone_admin_password"
      # SP26 — SFTP password via Docker secret (matches the admin-creds pattern).
      CYCLONE_SFTP_PASSWORD_FILE: "/run/secrets/cyclone_sftp_password"
      # Always bind to 0.0.0.0. The compose-managed bridge network
      # isolates the backend from the host's LAN/WAN — only the
      # `frontend` service joins it. Host firewall / port publishing
      # is the layer that controls what reaches us from outside the
      # compose network, not the bind address.
      CYCLONE_HOST: "0.0.0.0"
    secrets:
      - cyclone_db_key
      - cyclone_admin_username
      - cyclone_admin_password
      - cyclone_sftp_password
    volumes:
      - cyclone_db:/var/lib/cyclone/db
      - cyclone_backups:/var/lib/cyclone/backups
      - cyclone_prodfiles:/var/lib/cyclone/prodfiles
      - cyclone_sftp_staging:/var/lib/cyclone/sftp_staging
      - cyclone_logs:/var/log/cyclone
    healthcheck:
      test: ["CMD", "curl", "-fs", "http://localhost:8000/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    networks:
      - cyclone_network

  frontend:
    image: cyclone-frontend:${TAG:-stable}
    build:
      context: .
      dockerfile: Dockerfile.frontend
    restart: unless-stopped
    ports:
      # LAN-bind: only this port is published to the host. The operator
      # is expected to firewall this to the LAN subnet (or rely on VPN
      # for outside access). NO public TLS in v1.
      - "8080:8080"
    depends_on:
      backend:
        condition: service_healthy
    networks:
      - cyclone_network

secrets:
  cyclone_db_key:
    file: /etc/cyclone/secrets/db.key
  cyclone_admin_username:
    file: /etc/cyclone/secrets/admin_username
  cyclone_admin_password:
    file: /etc/cyclone/secrets/admin_pw
  cyclone_sftp_password:
    file: /etc/cyclone/secrets/sftp_password

volumes:
  cyclone_db:
  cyclone_backups:
  cyclone_prodfiles:
  cyclone_sftp_staging:
  cyclone_logs:

networks:
  cyclone_network:
    driver: bridge
