# vFusion from published images. # # NOT USABLE YET: nothing has been pushed to ghcr.io, so this file will # fail on pull. The workflow that builds the images is parked at # docs/ci/release.yml and needs a token that can write workflow files. # Use docker-compose.yml until then. # # docker compose -f docker-compose.release.yml --profile quick up -d # # The difference from docker-compose.yml is what happens on an upgrade. # That file builds from source: every update recompiles the frontend, # reinstalls node_modules and rebuilds the Python image on your # hardware, which is several minutes on a workstation and considerably # worse on a Pi. This one pulls images that CI already built, so an # upgrade is a download and a restart. # # It is otherwise the same stack, with two intentional differences: # # * The frontend is a static bundle behind nginx rather than a Vite # dev server, and that nginx proxies /api to the backend. The # dashboard and its API are therefore same-origin, so nothing here # needs CORS configured. # * Nothing bind-mounts the source, so editing files on the host has # no effect. Use docker-compose.yml if you are changing code. # # Deliberately a whole file rather than an override of the other one. # Compose appends volume lists instead of replacing them, so an override # cannot remove the source bind mounts, which is the entire point. # Duplication that works beats sharing that silently does not. name: vfusion # Which images to run. Pin this in anything you care about: `latest` # follows the newest release, which is fine until the morning a release # you have not read about restarts your stack. x-images: backend: &backend_image ghcr.io/packettrace/vfusion-backend:${VFUSION_TAG:-latest} frontend: &frontend_image ghcr.io/packettrace/vfusion-frontend:${VFUSION_TAG:-latest} services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: verkada POSTGRES_PASSWORD: verkada POSTGRES_DB: vfusion volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U verkada -d vfusion"] interval: 3s timeout: 3s retries: 10 redis: image: redis:7-alpine restart: unless-stopped healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 3s timeout: 3s retries: 10 backend: image: *backend_image restart: unless-stopped environment: DATABASE_URL: postgresql+asyncpg://verkada:verkada@postgres:5432/vfusion REDIS_URL: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY:-} FERNET_KEY: ${FERNET_KEY:-} # Same-origin by default here, so this only matters if you point # VFUSION_API_BASE at a backend on another host. CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:15173} PUBLIC_WEBHOOK_BASE: ${PUBLIC_WEBHOOK_BASE:-} # See docker-compose.yml: python:3.x-slim defaults to the C locale, # and any library that logs a non-ASCII character then crashes the # call that emitted it. PYTHONUTF8: "1" PYTHONIOENCODING: utf-8 LANG: C.UTF-8 LC_ALL: C.UTF-8 # "beta" (default), "stable", or "off" to stop checking GitHub for # new releases. See backend/app/updates.py. UPDATE_CHANNEL: ${UPDATE_CHANNEL:-beta} GEOIP_PROVIDER: ${GEOIP_PROVIDER:-ip-api} MQTT_INGEST_ENABLED: ${MQTT_INGEST_ENABLED:-} MQTT_HOST: ${MQTT_HOST:-mqtt-broker} MQTT_PORT: ${MQTT_PORT:-1883} MQTT_USERNAME: ${MQTT_USERNAME:-} MQTT_PASSWORD: ${MQTT_PASSWORD:-} MQTT_BROKER_HOST_PORT: ${MQTT_BROKER_HOST_PORT:-} RTSP_PUBLIC_PORT: ${RTSP_PUBLIC_PORT:-8554} ONVIF_PUBLIC_PORT: ${ONVIF_PUBLIC_PORT:-8090} ports: # Still published, so the API is reachable directly and existing # bookmarks and webhook URLs keep working. The dashboard does not # need it — nginx proxies. - "18080:8000" - "${ONVIF_PUBLIC_PORT:-8090}:8000" depends_on: postgres: condition: service_healthy redis: condition: service_healthy volumes: # No source mount. The code is in the image. - webhook_assets:/app/data - vfusion_secrets:/app/secrets - ./mqtt:/app/mqtt-host - ./rtsp:/app/rtsp-host # No --reload: there is nothing on disk for it to watch, and the # reloader costs a process and a file-system poll for nothing. command: > sh -c "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000" healthcheck: test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/api/health', timeout=2).status == 200 else 1)"] interval: 10s timeout: 3s retries: 5 start_period: 20s worker: image: *backend_image restart: unless-stopped environment: DATABASE_URL: postgresql+asyncpg://verkada:verkada@postgres:5432/vfusion REDIS_URL: redis://redis:6379/0 FERNET_KEY: ${FERNET_KEY:-} PYTHONUTF8: "1" PYTHONIOENCODING: utf-8 LANG: C.UTF-8 LC_ALL: C.UTF-8 depends_on: postgres: condition: service_healthy redis: condition: service_healthy # Migrations run in the backend's command. Without this the worker # can open a session against a schema that is one release behind # and fail on a column that exists in its code and not yet in the # database. backend: condition: service_healthy volumes: - webhook_assets:/app/data - vfusion_secrets:/app/secrets command: arq app.worker.WorkerSettings frontend: image: *frontend_image restart: unless-stopped environment: # Empty means same origin: the nginx serving this bundle proxies # /api, /hooks and /onvif to the backend. Set it only if the # backend lives somewhere this nginx cannot reach, and add this # dashboard's URL to CORS_ORIGINS if you do. VFUSION_API_BASE: ${VFUSION_API_BASE:-} ports: - "15173:8080" depends_on: - backend # ---- Optional profiles. Identical to docker-compose.yml. ---- cloudflared: image: cloudflare/cloudflared:latest restart: unless-stopped command: tunnel --no-autoupdate run environment: - TUNNEL_TOKEN=${CF_TUNNEL_TOKEN:-} depends_on: - backend profiles: ["cloudflared"] caddy-quick: image: caddy:2-alpine restart: unless-stopped volumes: - ./caddy/quick.Caddyfile:/etc/caddy/Caddyfile:ro depends_on: - backend profiles: ["quick"] cloudflared-quick: image: cloudflare/cloudflared:latest restart: unless-stopped command: tunnel --no-autoupdate --metrics 0.0.0.0:2000 --url http://caddy-quick:8080 depends_on: - caddy-quick profiles: ["quick"] mqtt-broker: image: eclipse-mosquitto:2 profiles: ["mqtt"] restart: unless-stopped volumes: - ./mqtt/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro - ./mqtt/passwd:/mosquitto/config/passwd:ro networks: [default] mqtt-tls: image: nginx:alpine profiles: ["mqtt"] restart: unless-stopped ports: - "443:443" volumes: - ./mqtt/nginx.conf:/etc/nginx/nginx.conf:ro - ./mqtt/certs:/etc/nginx/certs:ro depends_on: - mqtt-broker networks: [default] rtsp-server: image: bluenviron/mediamtx:latest profiles: ["rtsp"] restart: unless-stopped ports: - "${RTSP_PUBLIC_PORT:-8554}:8554" volumes: - ./rtsp:/rtsp command: ["/rtsp/mediamtx.yml"] networks: [default] volumes: postgres_data: webhook_assets: vfusion_secrets: