name: drivervault-aio # Production all-in-one, with SeaweedFS — pulls the prebuilt image from the # registry instead of building. 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 pull # 3. docker compose -f docker-compose.prod.seaweedfs.yml up -d # # 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. # # SeaweedFS runs as a second container beside the all-in-one, not as a fourth # process inside it: keeping the object store in that image, on the volume the # files are being moved off, would defeat the point and would mean rebuilding. # # 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 upserts the superuser from PB_ADMIN_*, and the API # Server creates any missing collections and the DriverVault super-admin from # DRIVERVAULT_SUPERADMIN_*. Both steps are idempotent. services: seaweedfs: image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" container_name: drivervault-aio-seaweedfs restart: unless-stopped # One process, four roles: master, volume, filer and the S3 gateway. -dir is # the only state it keeps. command: server -dir=/data -s3 -master.volumeSizeLimitMB=1024 environment: # SeaweedFS falls back to these when started without an -s3.config file, # and configuring one identity is what takes the S3 gateway out of its # default allow-anyone mode. The same credentials PocketBase authenticates # with below — one pair to set, in .env. AWS_ACCESS_KEY_ID: "${PB_S3_ACCESS_KEY:?set PB_S3_ACCESS_KEY in .env}" AWS_SECRET_ACCESS_KEY: "${PB_S3_SECRET:?set PB_S3_SECRET in .env}" volumes: # Named volume by default; set SEAWEED_DATA to a host path in .env for a # bind mount, exactly as PB_DATA works. Back it up alongside PB_DATA — # from here on the attachments live here, not in the database volume. - "${SEAWEED_DATA:-seaweed_data}:/data" ports: # Loopback only: the stack reaches the gateway over the compose network, # 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. - "${SEAWEED_S3_BIND:-127.0.0.1}:${SEAWEED_S3_PORT:-8333}:8333" healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status || exit 1"] interval: 10s timeout: 3s retries: 12 start_period: 20s seaweedfs-init: image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" container_name: drivervault-aio-seaweedfs-init # Runs once and exits. PocketBase never issues a CreateBucket of its own and # SeaweedFS will not conjure one on first upload, so something has to. # Creating a bucket that already exists is a no-op, so every later boot # passes straight through. restart: "no" depends_on: seaweedfs: condition: service_healthy entrypoint: ["/bin/sh", "-c"] # `|| true` so a restart is never blocked by the shell's exit status: this # step is best-effort, and a gateway that is genuinely unreachable is # reported by the API Server's own S3 check at boot, with the reason. command: - 'echo "s3.bucket.create -name ${PB_S3_BUCKET:-drivervault}" | weed shell -master=seaweedfs:9333 || true' drivervault: image: "${AIO_IMAGE:-10.2.1.10:5500/admin/drivervault-aio:latest}" container_name: drivervault-aio restart: unless-stopped depends_on: # PocketBase — inside this container — is the process that reads and # writes the objects, so the gateway has to be serving first, and the # bucket has to exist before the bootstrap points PocketBase at it. seaweedfs: condition: service_healthy seaweedfs-init: condition: service_completed_successfully 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 -------------------------------------------------- # 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. PB_S3_ENABLED: "true" PB_S3_BUCKET: "${PB_S3_BUCKET:-drivervault}" # The service name: a server-to-server call inside the compose network. PB_S3_ENDPOINT: "http://seaweedfs:8333" # SeaweedFS ignores the region; PocketBase insists on having one. PB_S3_REGION: "${PB_S3_REGION:-us-east-1}" PB_S3_ACCESS_KEY: "${PB_S3_ACCESS_KEY}" PB_S3_SECRET: "${PB_S3_SECRET}" # Path style, because a self-hosted gateway has no per-bucket DNS. PB_S3_FORCE_PATH_STYLE: "true" 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} volumes: # The only volume — named by default; set PB_DATA 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 here. - "${PB_DATA:-pb_data}:/pb/pb_data" healthcheck: # All three processes must answer. Declared here as well as in the image so # the check is visible, and works against an older pulled image. test: ["CMD-SHELL", "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: 5s retries: 3 start_period: 60s volumes: pb_data: seaweed_data: