# Cyclone — local docker-compose stack. # # Two services on a user-defined network: # # frontend (nginx) — published on http://127.0.0.1:8080 # serves the built SPA + reverse-proxies /api/* to backend # backend (uvicorn) — NOT published externally by default; reachable from # the frontend over the compose network on backend:8000 # (override `ports:` below to expose it for curl/debug) # # Persistent state: # - `cyclone-data` named volume mounted at /data in the backend holds # the SQLite database file. Survives `docker compose down`; only # `docker compose down -v` wipes it. # - `config/payers.yaml` is baked into the backend image at build time. # To edit payer config, change the YAML, rebuild the backend image, # and `POST /api/admin/reload-config` (or just restart the container). # # Usage: # docker compose up -d --build # start (or rebuild + restart) # docker compose logs -f # tail both services # docker compose restart backend # bounce the backend (e.g. after crash) # docker compose down # stop containers, KEEP the data volume # docker compose down -v # stop AND wipe the data volume name: cyclone services: backend: build: context: . dockerfile: backend/Dockerfile image: cyclone-backend:local container_name: cyclone-backend restart: unless-stopped environment: # Bind on all interfaces inside the container (required — 127.0.0.1 # would only be reachable from inside the container itself). CYCLONE_HOST: "0.0.0.0" CYCLONE_PORT: "8000" CYCLONE_RELOAD: "0" # Absolute path inside the container; the named volume mounts at /data. CYCLONE_DB_URL: "sqlite:////data/cyclone.db" # Bootstrap admin (required on first boot unless at least one user # already exists). The ${VAR:?msg} syntax makes docker-compose refuse # to start with a clear error if the env var isn't set in the host # environment. CYCLONE_ADMIN_USERNAME: ${CYCLONE_ADMIN_USERNAME:?CYCLONE_ADMIN_USERNAME is required on first boot} CYCLONE_ADMIN_PASSWORD: ${CYCLONE_ADMIN_PASSWORD:?CYCLONE_ADMIN_PASSWORD is required on first boot (min 12 chars)} volumes: - cyclone-data:/data # The healthcheck in the Dockerfile hits /api/health. The frontend # depends_on `service_healthy` so it won't accept traffic until the # backend is responsive. healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8000/api/health"] interval: 15s timeout: 5s retries: 3 start_period: 10s # No `ports:` — the frontend reaches the backend over the compose # network. Uncomment the next line to expose the API directly for # `curl http://localhost:8000/api/...` from the host. # ports: # - "127.0.0.1:8000:8000" networks: - cyclone-net frontend: build: context: . dockerfile: frontend/Dockerfile image: cyclone-frontend:local container_name: cyclone-frontend restart: unless-stopped depends_on: backend: condition: service_healthy ports: # Bind address defaults to 0.0.0.0 (reachable from the LAN). Set # CYCLONE_BIND_ADDRESS=127.0.0.1 to tighten to loopback-only and # match the standalone install's local-only posture. # Port defaults to 8081 to dodge the common clash on 8080; override # with CYCLONE_WEB_PORT=... (see README). - "${CYCLONE_BIND_ADDRESS:-0.0.0.0}:${CYCLONE_WEB_PORT:-8081}:80" healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/healthz"] interval: 15s timeout: 3s retries: 3 start_period: 5s networks: - cyclone-net networks: cyclone-net: driver: bridge volumes: cyclone-data: name: cyclone-data