name: drivervault-aio # Production all-in-one with SeaweedFS — and, unlike docker-compose.prod.yml, # built into ONE image here: Dockerfile.seaweedfs bakes `weed server -s3` # (master, volume, filer and S3 gateway in one process) into the same container # as PocketBase, the API Server and the Web App. One service, one container, # two volumes. Self-contained: one file, no overlays. Everything an operator # needs to set lives in .env. # # 1. cp .env.prod.seaweedfs.example .env (then edit it) # 2. docker compose -f docker-compose.prod.seaweedfs.yml build # 3. docker compose -f docker-compose.prod.seaweedfs.yml up -d # # The build context is the repo root, so this has to run on a host that has the # checkout. To deploy elsewhere, `push` the built image to the registry named in # AIO_IMAGE and `pull` it there — `image:` and `build:` are both set, so the # same file does either. # # This is docker-compose.prod.yml plus an S3 object store: PocketBase keeps its # record files — document scans, service and refill receipts, workshop invoices, # part photos — in a SeaweedFS bucket instead of on the pb_data volume next to # the database. The database and PocketBase's own backups stay on PB_DATA. # Clients cannot tell the difference: an attachment has always been fetched # through the API Server, never from a storage URL. # # What baking it in costs against the two-container shape it replaces: a new # SeaweedFS is a rebuild (SEAWEED_VERSION below), not a tag change. For # per-role restart, metrics and the SeaweedFS admin UI, see # docker-compose.prod.seaweedfs.split.yml — the S3 behaviour is identical, and # the data volume is interchangeable (both write the `weed server -dir` layout). # # Before turning this on for a stack that already has uploads: PocketBase does # NOT copy existing files into the bucket. See README.md. # # On first boot PocketBase's program creates the bucket and upserts the # superuser from PB_ADMIN_*, and the API Server creates any missing # collections, the DriverVault super-admin from DRIVERVAULT_SUPERADMIN_*, and # points PocketBase's file storage at the bucket. Every step is idempotent. services: drivervault: build: # Project root (one level up from this compose file), so the Dockerfile # can reach both "API Server/" and "Web App/". context: .. dockerfile: Docker-AIO/Dockerfile.seaweedfs args: # Empty -> bundle uses same-origin "/api", proxied internally by nginx. - VITE_API_BASE=${VITE_API_BASE:-} # Bare names = pass through only when set in the environment, so an # unset PB_VERSION / SEAWEED_VERSION leaves the Dockerfile pin in place # instead of overriding it with an empty string (which for PB_VERSION # would resolve "latest" at build time, and for SEAWEED_VERSION would # not build at all). - PB_VERSION - SEAWEED_VERSION # Tagged for the registry so `docker compose push` lands it where `pull` # on the deploy host expects it. image: "${AIO_IMAGE:-10.2.1.10:5500/admin/drivervault-aio-seaweedfs:latest}" container_name: drivervault-aio restart: unless-stopped environment: # Superuser (also used by the API Server to authenticate to PocketBase). PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL:?set PB_ADMIN_EMAIL in .env}" PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD:?set PB_ADMIN_PASSWORD in .env}" # Match CORS to the web origin (only used if a browser calls the API directly). CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" # Probed by the panel status page. nginx serves the Web App on port 80 # inside this container, so plain localhost:8090 would never answer. # Override WEBAPP_URL in .env to make a change from the panel's Web App # screen permanent; the panel alone only holds it for the container's life. WEBAPP_URL: "${WEBAPP_URL:-http://127.0.0.1:80}" # Schema + super-admin bootstrap (idempotent). Leave this ON: a release can # add collections or fields the server needs, and a stack that skips the # bootstrap never gets them. The API Server self-heals exactly one thing — # app_settings, the collection holding the plugin settings, which it # creates on demand because it cannot serve the plugin panel without it. # Every other schema change still depends on this flag. Turn it off only # for a database you know already matches the release. 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). This image serves plain HTTP, so a # charger can only connect when TLS is terminated in front of it (set # OCPP_PUBLIC_URL to the public wss:// base) — or, on a trusted network, # with OCPP_REQUIRE_TLS=false. OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" # --- File storage -------------------------------------------------- # The image fixes the rest (PB_S3_ENABLED, the loopback endpoint, path # style, the region): the gateway is inside, at an address that cannot # change. supervisord passes these through to the API Server, whose # bootstrap writes them into PocketBase's settings on every boot, # idempotently. Only record files move — scans, receipts, invoices, part # photos. The database and PocketBase's own backups stay on PB_DATA. # # The credentials do double duty: they configure the gateway's single # identity — which is what takes it out of its default allow-anyone mode # — *and* they are what PocketBase authenticates with. No safe defaults; # the gateway refuses to start without them, and with it the whole # container. PB_S3_BUCKET: "${PB_S3_BUCKET:-drivervault}" PB_S3_ACCESS_KEY: "${PB_S3_ACCESS_KEY:?set PB_S3_ACCESS_KEY in .env}" PB_S3_SECRET: "${PB_S3_SECRET:?set PB_S3_SECRET in .env}" ports: - "${WEB_PORT:-8090}:80" # Web App - "${PB_PORT:-8070}:8070" # PocketBase admin UI / API - "${API_PORT:-8080}:8080" # API Server + panel (root /) + /ocpp/{serial} # The S3 gateway, loopback only: PocketBase reaches it inside the # container, so this is here for `aws s3 ls --endpoint-url # http://127.0.0.1:8333` and nothing else. Set SEAWEED_S3_BIND=0.0.0.0 to # expose it, and mean it. Nothing else of SeaweedFS is published: the # volume server (8081 inside) serves file content by id with no # authentication at all. - "${SEAWEED_S3_BIND:-127.0.0.1}:${SEAWEED_S3_PORT:-8333}:8333" volumes: # Named volumes by default; set either to a host path in .env for a bind # mount. The API Server keeps no state on disk, so everything it owns # (plugin settings included) is in PB_DATA. Back up PB_DATA and # SEAWEED_DATA together — from here on the attachments live in the # second, not the first. - "${PB_DATA:-pb_data}:/pb/pb_data" - "${SEAWEED_DATA:-seaweed_data}:/seaweed/data" healthcheck: # Every process must answer — SeaweedFS's master and gateway and the three # app processes. Declared here as well as in the image so the check is # visible, and works against an older pulled image. start_period covers # SeaweedFS coming up plus the first-boot schema bootstrap. test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null && wget -qO- http://127.0.0.1:8333/healthz >/dev/null && wget -qO- http://127.0.0.1:8070/api/health >/dev/null && wget -qO- http://127.0.0.1:8080/healthz >/dev/null && wget -qO- http://127.0.0.1:80/healthz >/dev/null || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 90s volumes: pb_data: # The `weed server -dir` layout, so this file and its .split. twin can swap # places on it. seaweed_data: