name: drivervault # Production DriverVault stack — pulls prebuilt images from the registry instead # of building from source. Everything an operator needs to set lives in .env. # # 1. cp .env.prod.example .env (then edit it — all secrets/ports/volumes) # 2. docker compose -f docker-compose.prod.yml pull # 3. docker compose -f docker-compose.prod.yml up -d # # Traffic flow (browser): Web App BFF --/api--> API Server --> PocketBase. # # On first boot: # • PocketBase upserts the superuser from PB_ADMIN_* (create-if-missing). # • the API Server creates any missing collections, reconciles existing ones, # and creates the DriverVault super-admin from DRIVERVAULT_SUPERADMIN_*. # Both steps are idempotent, so restarts and upgrades are safe. services: pocketbase: image: "${PB_IMAGE:-10.2.1.10:5500/admin/drivervault-pocketbase:latest}" container_name: drivervault-pocketbase restart: unless-stopped environment: # The superuser is created/updated on boot (the API Server authenticates # with it). This is the only place the first superuser can be created — the # REST API cannot bootstrap it. PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL:?set PB_ADMIN_EMAIL in .env}" PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD:?set PB_ADMIN_PASSWORD in .env}" volumes: # Named volume by default; set PB_DATA to a host path in .env for a bind mount. - "${PB_DATA:-pb_data}:/pb/pb_data" ports: # Bound to localhost by default — the admin UI (/_/) is reachable only on # the host. Set PB_BIND=0.0.0.0 in .env to expose it on the network. - "${PB_BIND:-127.0.0.1}:${PB_PORT:-8070}:8070" healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8070/api/health || exit 1"] interval: 10s timeout: 3s retries: 12 start_period: 10s api-server: image: "${API_IMAGE:-10.2.1.10:5500/admin/drivervault-api-server:latest}" container_name: drivervault-api restart: unless-stopped depends_on: pocketbase: condition: service_healthy environment: API_ADDR: ":8080" # Reach PocketBase by its service name on the internal network. POCKETBASE_URL: "http://pocketbase:8070" POCKETBASE_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" POCKETBASE_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" # Probed by the panel status page. This is a server-to-server call inside # the compose network, so it must be the service name — the default # (localhost:8090) would resolve to this container itself. WEBAPP_URL: "http://web-app:8090" CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" # Schema + super-admin bootstrap (idempotent). Set PB_BOOTSTRAP=false to # skip it once the database is established. PB_BOOTSTRAP: "${PB_BOOTSTRAP:-true}" DRIVERVAULT_SUPERADMIN_EMAIL: "${DRIVERVAULT_SUPERADMIN_EMAIL:-}" DRIVERVAULT_SUPERADMIN_PASSWORD: "${DRIVERVAULT_SUPERADMIN_PASSWORD:-}" DRIVERVAULT_SUPERADMIN_NAME: "${DRIVERVAULT_SUPERADMIN_NAME:-Administrator}" # OCPP charger control (Anker Solix). Chargers are rejected unless they # reach the server over TLS. Behind a TLS-terminating reverse proxy, set # OCPP_PUBLIC_URL to the public wss:// base and API_BIND so the proxy can # reach this port; only drop OCPP_REQUIRE_TLS on a trusted network. OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" ports: # Localhost-only by default (the Web App reaches it over the internal # network). Set API_BIND=0.0.0.0 to expose the API panel — and the # /ocpp/{serial} endpoint chargers dial into — on the network. - "${API_BIND:-127.0.0.1}:${API_PORT:-8080}:8080" volumes: # The .env the panel writes back when a superadmin retargets PocketBase — # see the API Server Dockerfile. Plugin settings live in the database, so # they no longer depend on this volume. - "${API_DATA:-api_data}:/data" healthcheck: # Declared here rather than relying only on the image's HEALTHCHECK, so the # depends_on gate below still works against an older pulled image. test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"] interval: 10s timeout: 3s retries: 12 start_period: 20s web-app: image: "${WEB_IMAGE:-10.2.1.10:5500/admin/drivervault-web-app:latest}" container_name: drivervault-web restart: unless-stopped depends_on: # The image now ships a HEALTHCHECK, so wait for the API Server to be # serving rather than merely started. api-server: condition: service_healthy environment: # The BFF reverse-proxies /api/* to the API Server over the internal network. API_BASE: "http://api-server:8080" ports: # The public front door. Bound on all interfaces so browsers can reach it. - "${WEB_PORT:-8090}:8090" healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8090/healthz || exit 1"] interval: 10s timeout: 3s retries: 12 start_period: 10s volumes: pb_data: api_data: