name: mediahub x-logging: &logging driver: json-file options: max-size: "10m" max-file: "5" x-backend-env: &backend-env MEDIAHUB_DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} MEDIAHUB_STORAGE_ROOT: /data/storage MEDIAHUB_ENVIRONMENT: ${MEDIAHUB_ENVIRONMENT:-production} MEDIAHUB_TIMEZONE: ${MEDIAHUB_TIMEZONE:-Asia/Taipei} MEDIAHUB_MAX_UPLOAD_BYTES: ${MEDIAHUB_MAX_UPLOAD_BYTES:-21474836480} MEDIAHUB_MIN_FREE_BYTES: ${MEDIAHUB_MIN_FREE_BYTES:-2147483648} MEDIAHUB_FFMPEG_THREADS: ${MEDIAHUB_FFMPEG_THREADS:-4} MEDIAHUB_MASTER_KEY: ${MEDIAHUB_MASTER_KEY:?run scripts/init-env.sh (MEDIAHUB_MASTER_KEY missing)} MEDIAHUB_ALLOW_SIMULATED_NODES: ${MEDIAHUB_ALLOW_SIMULATED_NODES:-false} MEDIAHUB_PUBLIC_URL: ${MEDIAHUB_PUBLIC_URL:-} MEDIAHUB_NODE_OFFLINE_AFTER_SECONDS: ${MEDIAHUB_NODE_OFFLINE_AFTER_SECONDS:-90} MEDIAHUB_NODE_HEARTBEAT_SECONDS: ${MEDIAHUB_NODE_HEARTBEAT_SECONDS:-30} TZ: ${MEDIAHUB_TIMEZONE:-Asia/Taipei} services: db: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 20 logging: *logging api: build: context: ./backend target: base image: mediahub-api:${HUB_IMAGE_TAG:-latest} restart: unless-stopped environment: *backend-env volumes: - storage:/data/storage # Public root CA certificate only (exported by the caddy container) — lets pairing # payloads carry the CA fingerprint for pinning. Caddy's private keys stay in caddy_data. - ca_public:/ca-public:ro # Android storage-node app, built on the host (scripts/build-android.sh), served to admins. - ./android/dist:/app/android-dist:ro depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=4)"] interval: 10s timeout: 5s retries: 12 start_period: 20s mem_limit: 1g logging: *logging worker: image: mediahub-api:${HUB_IMAGE_TAG:-latest} restart: unless-stopped command: ["python", "-m", "app.worker"] environment: *backend-env volumes: - storage:/data/storage depends_on: api: condition: service_healthy # Transcoding is CPU-heavy: cap it so the rest of the machine stays responsive. cpus: ${MEDIAHUB_WORKER_CPUS:-4} mem_limit: 3g stop_grace_period: 40s logging: *logging caddy: build: context: . dockerfile: deploy/caddy/Dockerfile image: mediahub-web:${HUB_IMAGE_TAG:-latest} restart: unless-stopped environment: HUB_SITES: ${HUB_SITES:-https://localhost} HUB_DEFAULT_SNI: ${HUB_DEFAULT_SNI:-localhost} HUB_MAX_UPLOAD: ${HUB_MAX_UPLOAD:-20GB} ports: # Docker-published ports bypass ufw. Default binds to loopback only; set # HUB_BIND to this machine's LAN IP to serve phones/PCs on the same network. - "${HUB_BIND:-127.0.0.1}:${HUB_HTTPS_PORT:-443}:443" volumes: - caddy_data:/data - caddy_config:/config - ca_public:/ca-public depends_on: api: condition: service_healthy logging: *logging # `docker compose --profile test run --rm tests` — isolated DB + temp storage. tests: profiles: ["test"] build: context: ./backend target: test image: mediahub-api:test environment: MEDIAHUB_DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/mediahub_test MEDIAHUB_STORAGE_ROOT: /tmp/mediahub-test MEDIAHUB_ENVIRONMENT: test MEDIAHUB_COOKIE_SECURE: "false" MEDIAHUB_ALLOW_SIMULATED_NODES: "true" depends_on: db: condition: service_healthy volumes: pgdata: storage: caddy_data: caddy_config: ca_public: