diff --git a/Docker-AIO/.env.prod.seaweedfs.split.example b/Docker-AIO/.env.prod.seaweedfs.split.example new file mode 100644 index 0000000..db150d0 --- /dev/null +++ b/Docker-AIO/.env.prod.seaweedfs.split.example @@ -0,0 +1,129 @@ +# DriverVault all-in-one — production config, SeaweedFS split into its four roles. +# Copy to .env and fill in, then: +# docker compose -f docker-compose.prod.seaweedfs.split.yml pull +# docker compose -f docker-compose.prod.seaweedfs.split.yml up -d + +# --- Registry image ---------------------------------------------------------- +AIO_IMAGE=10.2.1.10:5500/admin/drivervault-aio:latest + +# --- PocketBase superuser (required) ----------------------------------------- +# Created/updated on first boot. The API Server uses these to manage the database. +PB_ADMIN_EMAIL=admin@example.com +PB_ADMIN_PASSWORD=change-me-long-password + +# --- DriverVault super-admin (app login) ------------------------------------- +# The first application user, created by the API Server on boot with role +# "superadmin" if no user with this email exists yet. Leave blank to skip. +DRIVERVAULT_SUPERADMIN_EMAIL=owner@example.com +DRIVERVAULT_SUPERADMIN_PASSWORD=change-me-long-password +DRIVERVAULT_SUPERADMIN_NAME=Administrator + +# Schema creation/reconcile on boot. Leave this true: a release can add +# collections or fields the server needs, and a stack that skips the bootstrap +# never gets them. (The API Server creates app_settings, which holds the plugin +# settings, on demand — but only that one.) Set false only for a database you +# know already matches the release. +PB_BOOTSTRAP=true + +# Allowed CORS origin(s) — match your public web URL / WEB_PORT. +CORS_ALLOW_ORIGINS=http://localhost:8090 + +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. This image serves plain HTTP, so +# terminate TLS in a reverse proxy in front of it and set OCPP_PUBLIC_URL to the +# public wss:// base the charger should be pointed at. Turning the check off is +# for trusted networks only. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + +# --- Host port mappings (optional; defaults shown) -------------------------- +WEB_PORT=8090 +PB_PORT=8070 +API_PORT=8080 + +# --- Settings the API Server panel can also change --------------------------- +# The panel's Settings screens apply these immediately, but only for the life of +# the container — the value below is re-applied on every restart and wins. Set it +# here to make a change permanent. (POCKETBASE_URL is fixed to this container's +# own PocketBase and is not meant to be repointed.) +# WEBAPP_URL the Web App address the panel status page probes; nginx +# serves it on port 80 inside this container. +# CORS_ALLOW_ORIGINS browser origins allowed to call the API Server directly. +# WEBAPP_URL=http://127.0.0.1:80 + +# --- Storage ----------------------------------------------------------------- +# One Docker-managed named volume by default. Set it to an absolute host path +# for a bind mount, e.g. PB_DATA=/srv/drivervault/pb_data. +# PB_DATA — the PocketBase database and its backups. The API Server keeps no +# state on disk, so everything it owns (plugin settings included) is backed up +# by backing up this one path — together with SEAWEED_DATA below. +PB_DATA=pb_data + +# --- File storage: SeaweedFS ------------------------------------------------- +# PocketBase keeps its record files — document scans, service and refill +# receipts, workshop invoices, part photos — in the bucket below instead of on +# PB_DATA. The database and PocketBase's own backups stay where they are. +# +# The credentials do double duty: seaweedfs-init writes them into the filer's +# IAM store as the identity named "drivervault" *and* they are what PocketBase +# authenticates with. There are no safe defaults, and the stack refuses to start +# without them. Change them here and restart to rotate: the seed updates the +# identity in place rather than adding a second one. +PB_S3_ACCESS_KEY= +PB_S3_SECRET= +# The bucket. Created on first boot by the seaweedfs-init container. +PB_S3_BUCKET=drivervault +# SeaweedFS ignores the region; PocketBase insists on having one. +PB_S3_REGION=us-east-1 + +# SEAWEED_DATA — where SeaweedFS keeps the files. A Docker-managed named volume +# by default; set an absolute host path for a bind mount, the same way PB_DATA +# works above. Back it up alongside PB_DATA: from here on the attachments live +# here, not in the database volume. +# +# The master, volume and filer containers all mount it at /data, which is the +# layout `weed server -dir=/data` writes — so this file and +# docker-compose.prod.seaweedfs.yml are interchangeable on the same volume, with +# nothing to migrate either way. +SEAWEED_DATA=seaweed_data +# The SeaweedFS image, pinned so a redeploy months from now brings up the same +# one. All five SeaweedFS containers run it. +# SEAWEED_IMAGE=chrislusf/seaweedfs:4.45 +# The S3 port is published on loopback only — the stack reaches the gateway over +# the compose network, and this is for tools like aws-cli. Set +# SEAWEED_S3_BIND=0.0.0.0 to expose it to other hosts, and mean it. +# SEAWEED_S3_BIND=127.0.0.1 +# SEAWEED_S3_PORT=8333 +# +# The master, volume and filer publish no host port at all. The admin UI below +# shows what they would: the volume server in particular serves file content by +# id with no authentication, so it stays on the compose network. Reach the +# others with `docker compose exec`. + +# --- SeaweedFS admin UI ------------------------------------------------------ +# Cluster topology, volumes, buckets, maintenance tasks, and Object Store → +# Users, where further S3 identities are created and revoked. They land in the +# filer's IAM store, the same one seeded above, and the gateway picks them up +# without a restart. +# +# REQUIRED: weed disables authentication entirely when the password is empty, +# and this panel can mint credentials for the bucket. +SEAWEED_ADMIN_USER=admin +SEAWEED_ADMIN_PASSWORD= +# Optional view-only login. +SEAWEED_ADMIN_READONLY_USER= +SEAWEED_ADMIN_READONLY_PASSWORD= +# Bound to localhost by default, the same call SEAWEED_S3_BIND makes: storage +# plumbing, not one of the app's own panels. On a remote host that means +# unreachable — set 0.0.0.0 and put it behind a reverse proxy. +SEAWEED_ADMIN_BIND=127.0.0.1 +SEAWEED_ADMIN_PORT=23646 +# Its own small volume: session key and maintenance-task state, no object data. +SEAWEED_ADMIN_DATA=seaweed_admin + +# Existing uploads are NOT migrated when this is switched on: PocketBase copies +# nothing, so attachments made before the switch stop resolving. Read the file +# storage section of README.md first. diff --git a/Docker-AIO/.env.seaweedfs.split.example b/Docker-AIO/.env.seaweedfs.split.example new file mode 100644 index 0000000..e1c8444 --- /dev/null +++ b/Docker-AIO/.env.seaweedfs.split.example @@ -0,0 +1,107 @@ +# Copy to .env and fill in. Used by the Docker-AIO +# docker-compose.seaweedfs.split.yml — the same stack as .env.seaweedfs.example, +# with SeaweedFS running as separate master / volume / filer / S3 containers +# plus the SeaweedFS admin UI. + +# --- Required (no defaults) -------------------------------------------------- +# PocketBase superuser, also used by the API Server to authenticate. +PB_ADMIN_EMAIL=admin@example.com +PB_ADMIN_PASSWORD=change-me-long-password + +# --- DriverVault super-admin (app login) ------------------------------------- +# The first application user, created by the API Server on boot with role +# "superadmin" if no user with this email exists yet. Leave blank to skip. +DRIVERVAULT_SUPERADMIN_EMAIL=owner@example.com +DRIVERVAULT_SUPERADMIN_PASSWORD=change-me-long-password +DRIVERVAULT_SUPERADMIN_NAME=Administrator + +# Schema creation/reconcile on boot. Leave this true: a release can add +# collections or fields the server needs, and a stack that skips the bootstrap +# never gets them. (The API Server creates app_settings, which holds the plugin +# settings, on demand — but only that one.) Set false only for a database you +# know already matches the release. +PB_BOOTSTRAP=true + +# Allowed CORS origin(s) — match your web origin / WEB_PORT. +CORS_ALLOW_ORIGINS=http://localhost:8090 + +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. This image serves plain HTTP: +# either terminate TLS in front of it and set OCPP_PUBLIC_URL to the public +# wss:// base, or set OCPP_REQUIRE_TLS=false on a trusted network. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + +# --- Host port mappings (optional; defaults shown) -------------------------- +WEB_PORT=8090 +PB_PORT=8070 +# API Server + its embedded web panel (served at the API root, http://host:8080/). +API_PORT=8080 + +# --- Build args (optional) --------------------------------------------------- +# Leave empty so the browser uses same-origin /api (proxied by nginx). +VITE_API_BASE= +# PocketBase version. The Dockerfile already pins one; set this only to build a +# different version. Leaving it commented out keeps the pin (an empty value here +# is passed through as-is and would resolve the latest release at build time). +#PB_VERSION=0.39.11 + +# --- Settings the API Server panel can also change --------------------------- +# The panel's Settings screens apply these immediately, but only for the life of +# the container — the value below is re-applied on every restart and wins. Set it +# here to make a change permanent. (POCKETBASE_URL is fixed to this container's +# own PocketBase and is not meant to be repointed.) +# WEBAPP_URL the Web App address the panel status page probes; nginx +# serves it on port 80 inside this container. +# CORS_ALLOW_ORIGINS browser origins allowed to call the API Server directly. +# WEBAPP_URL=http://127.0.0.1:80 + +# --- File storage: SeaweedFS ------------------------------------------------- +# PocketBase keeps its record files — document scans, service and refill +# receipts, workshop invoices, part photos — in the bucket below instead of on +# pb_data. The database and PocketBase's own backups stay where they are. +# +# The credentials do double duty: seaweedfs-init writes them into the filer's +# IAM store as the identity named "drivervault" *and* they are what PocketBase +# authenticates with. There are no safe defaults, and the stack refuses to start +# without them. Change them here and restart to rotate: the seed updates the +# identity in place rather than adding a second one. +PB_S3_ACCESS_KEY= +PB_S3_SECRET= +# The bucket. Created on first boot by the seaweedfs-init container. +PB_S3_BUCKET=drivervault +# SeaweedFS ignores the region; PocketBase insists on having one. +PB_S3_REGION=us-east-1 +# The SeaweedFS image, pinned so a rebuild months from now brings up the same +# one. All five SeaweedFS containers run it. +# SEAWEED_IMAGE=chrislusf/seaweedfs:4.45 + +# --- SeaweedFS admin UI ------------------------------------------------------ +# http://localhost:23646 — cluster topology, volumes, buckets, and +# Object Store → Users, where further S3 identities are created and revoked. +# They land in the filer's IAM store, the same one seeded above, and the gateway +# picks them up without a restart. +# +# REQUIRED: weed disables authentication entirely when the password is empty, +# and this panel can mint credentials for the bucket. +SEAWEED_ADMIN_USER=admin +SEAWEED_ADMIN_PASSWORD= +# SEAWEED_ADMIN_PORT=23646 + +# --- SeaweedFS host ports (optional; defaults shown) ------------------------- +# Published for aws-cli, `weed shell` and poking around while developing. The +# stack itself reaches every one of these over the compose network. +# Note SEAWEED_VOLUME_PORT: the volume server serves file content by id with NO +# authentication, so do not carry this mapping over to a shared machine. It +# lands on 8081 because API_PORT already has 8080. +# SEAWEED_MASTER_PORT=9333 +# SEAWEED_VOLUME_PORT=8081 +# SEAWEED_FILER_PORT=8888 +# SEAWEED_S3_PORT=8333 + +# Existing uploads are NOT migrated when this is switched on: PocketBase copies +# nothing, so attachments made before the switch stop resolving. Read the file +# storage section of README.md first. diff --git a/Docker-AIO/README.md b/Docker-AIO/README.md index dfafb45..10145ca 100644 --- a/Docker-AIO/README.md +++ b/Docker-AIO/README.md @@ -105,6 +105,7 @@ remember — with an `.env` example of the same name: |---|---|---| | **Local storage** — the default, unchanged | `docker-compose.prod.yml` | `docker-compose.yml` | | **SeaweedFS beside the image** | `docker-compose.prod.seaweedfs.yml` | `docker-compose.seaweedfs.yml` | +| **SeaweedFS, split into its roles** | `docker-compose.prod.seaweedfs.split.yml` | `docker-compose.seaweedfs.split.yml` | | **An S3 endpoint elsewhere** | `docker-compose.prod.s3.yml` | `docker-compose.s3.yml` | So `docker-compose.prod.seaweedfs.yml` is configured from @@ -127,6 +128,41 @@ bucket, because PocketBase never issues a `CreateBucket` of its own. The external-S3 ones add no containers at all: set `PB_S3_ENDPOINT`, and create the bucket yourself. +### Split SeaweedFS + +`weed server -s3` runs master, volume, filer and gateway as four goroutines in +one process. The `.split.` files run them as four containers beside the +all-in-one, plus a fifth: the SeaweedFS **admin UI** on port 23646, where the +cluster can be inspected and — under *Object Store → Users* — further S3 +identities minted and revoked. Split also gets you per-role restarts and +upgrades, per-role Prometheus metrics, and room to add a second volume server +later. Still none of them inside the image, for the reason above. + +Identities work differently there, and it matters. SeaweedFS reads credentials +from, in descending priority: an `-s3.config` file, the filer's IAM store, then +`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` — and a higher source *replaces* a +lower one rather than adding to it. The single-gateway files use the env vars, +which is why nothing else may write identities there: the first user added in a +panel would displace PocketBase's key. So in the split files `seaweedfs-init` +seeds PocketBase's identity into the filer's store instead — the same store the +admin UI writes — and the gateway runs with no config file at all. One source of +truth, PocketBase's key visible in the panel beside every other, and new keys +picked up without a restart. Rotating `PB_S3_SECRET` in `.env` and restarting +updates that identity in place. + +Set `SEAWEED_ADMIN_PASSWORD`: `weed admin` serves the panel with no +authentication when it is empty, and a panel that can mint bucket credentials is +the bucket. In the prod file it is bound to loopback like `SEAWEED_S3_BIND` — it +is storage plumbing, not one of the app's own panels — so a remote host needs +`SEAWEED_ADMIN_BIND=0.0.0.0` behind a reverse proxy. That file publishes nothing +for master, volume and filer: the volume server serves file content by id with +no authentication of any kind, and the admin UI already shows what those ports +would. + +Switching between `docker-compose.seaweedfs.yml` and its `.split.` twin needs no +migration: master, volume and filer share one `/data` mount, which is exactly +the layout `weed server -dir=/data` writes. + On every boot the API Server's bootstrap writes PocketBase's *Files storage* settings from those variables, then asks PocketBase to prove it can reach the bucket. Watch for it in the log: diff --git a/Docker-AIO/docker-compose.prod.seaweedfs.split.yml b/Docker-AIO/docker-compose.prod.seaweedfs.split.yml new file mode 100644 index 0000000..f526c90 --- /dev/null +++ b/Docker-AIO/docker-compose.prod.seaweedfs.split.yml @@ -0,0 +1,330 @@ +name: drivervault-aio + +# Production all-in-one, with SeaweedFS split into its four roles — 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.split.example .env (then edit it) +# 2. docker compose -f docker-compose.prod.seaweedfs.split.yml pull +# 3. docker compose -f docker-compose.prod.seaweedfs.split.yml up -d +# +# This is docker-compose.prod.seaweedfs.yml with the storage layer taken apart. +# `weed server -s3` runs master, volume, filer and gateway as goroutines in one +# process; here each is its own container, plus the SeaweedFS admin UI. What +# that buys: +# +# • the admin UI (weed admin) — a cluster view, and Object Store → Users, +# where S3 identities are created and revoked without touching a file; +# • per-role restart, upgrade and Prometheus metrics; +# • room to add a second volume server later, on this host or another. +# +# What it costs: five containers beside the all-in-one instead of one, five +# healthchecks to keep the boot order honest, and one more port worth binding +# carefully. If none of the above is wanted, use +# docker-compose.prod.seaweedfs.yml — the S3 behaviour is identical. +# +# None of them run inside the all-in-one image, for the same reason the single +# gateway does not: keeping the object store in that image, on the volume the +# files are being moved off, would defeat the point and would mean rebuilding. +# +# The on-disk layout is deliberately the same as the single-process file's: +# master, volume and filer share one /data mount, exactly as `weed server -dir` +# lays it out (master raft state, volume .dat/.idx, the filer's filerldb2/ — no +# filename overlap). So the two files are interchangeable on the same +# SEAWEED_DATA, with no migration either way. A *second* volume server would +# need its own. +# +# Only the S3 gateway and the admin UI publish a host port, both on loopback, +# the way the single-gateway file publishes the S3 port. Master, volume and +# filer are reachable over the compose network, through the admin UI, or with +# `docker compose exec` — the volume server in particular serves file content by +# id with no authentication at all, so it has no business on a public interface. +# +# 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: master ----------------------------------------------------- + # Keeps the volume/topology metadata and hands out file ids. -ip is the name + # the other roles are told to reach it by, so it must be the service name and + # not the container IP the process would otherwise detect. + seaweedfs-master: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-master + restart: unless-stopped + command: > + master -ip=seaweedfs-master -ip.bind=0.0.0.0 -mdir=/data + -volumeSizeLimitMB=1024 -metricsPort=9324 + 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" + # No published port: the master UI is one of the pages the admin UI serves. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: volume server --------------------------------------------- + # Where the bytes actually land. -max=0 lets it size itself from free disk + # rather than the default cap of 8 volumes. + seaweedfs-volume: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-volume + restart: unless-stopped + command: > + volume -master=seaweedfs-master:9333 -ip=seaweedfs-volume -ip.bind=0.0.0.0 + -port=8080 -dir=/data -max=0 -metricsPort=9325 + depends_on: + seaweedfs-master: + condition: service_healthy + volumes: + - "${SEAWEED_DATA:-seaweed_data}:/data" + # No published port, and this one is not an oversight: 8080 serves file + # content by file id with NO authentication — the S3 credentials do not + # apply to it. Publishing it would publish every attachment in the stack. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: filer ------------------------------------------------------ + # Gives the flat volume store a directory tree — buckets, object keys — and + # holds the S3 identities the admin UI writes. -defaultStoreDir is where its + # embedded leveldb goes; without it that would be the container's working + # directory, and the identities would not survive a recreate. + seaweedfs-filer: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-filer + restart: unless-stopped + command: > + filer -master=seaweedfs-master:9333 -ip=seaweedfs-filer -ip.bind=0.0.0.0 + -port=8888 -defaultStoreDir=/data -metricsPort=9326 + depends_on: + seaweedfs-volume: + condition: service_healthy + volumes: + - "${SEAWEED_DATA:-seaweed_data}:/data" + # No published port. The filer's gRPC side (8888 + 10000) carries the IAM + # service that mints S3 credentials; keep both ends of it on the compose + # network. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8888/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: bucket and identity seed ---------------------------------- + # Runs once and exits, before the gateway starts. Two jobs: + # + # 1. create the bucket — PocketBase never issues a CreateBucket of its own; + # 2. write PocketBase's S3 identity into the filer's IAM store. + # + # (2) is why this stack does not set AWS_ACCESS_KEY_ID on the gateway, the way + # docker-compose.prod.seaweedfs.yml does. Those env vars are the *lowest* + # priority credential source in SeaweedFS: they are read only while the filer's + # store is empty, so the first identity added in the admin UI would silently + # displace them and lock PocketBase out. Seeding the store the admin UI itself + # writes leaves one source of truth, and the key PocketBase uses appears under + # Object Store → Users like any other. + # + # Both commands update in place, so every later boot re-applies the values from + # .env and changes nothing else — which is also how a rotated PB_S3_SECRET + # reaches the gateway. + # + # The closing grep is the gate: an empty IAM store means the gateway would come + # up in its allow-anyone default, so this fails loudly instead and the gateway + # below never starts. No `|| true` here, deliberately. + seaweedfs-init: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-init + restart: "no" + depends_on: + seaweedfs-filer: + condition: service_healthy + environment: + # Passed as env and expanded by the shell inside the container, so the + # secret stays out of the container's argv. + 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}" + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + printf '%s\n' \ + "s3.bucket.create -name $$PB_S3_BUCKET" \ + "s3.configure -user drivervault -access_key $$PB_S3_ACCESS_KEY -secret_key $$PB_S3_SECRET -actions Admin -apply" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 + echo "s3.configure" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 \ + | grep -q "$$PB_S3_ACCESS_KEY" + + # --- SeaweedFS: S3 gateway ------------------------------------------------- + # The endpoint PocketBase talks to. No -config file: with only -filer given, + # credentials come from the filer's IAM store, which is what lets the admin UI + # add and revoke identities without a restart. A config file would take + # priority over that store and make the admin UI's users inert. + seaweedfs-s3: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-s3 + restart: unless-stopped + command: > + s3 -filer=seaweedfs-filer:8888 -ip.bind=0.0.0.0 -port=8333 + -metricsPort=9327 + depends_on: + seaweedfs-filer: + condition: service_healthy + # Never serve before an identity exists — see seaweedfs-init above. + seaweedfs-init: + condition: service_completed_successfully + 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:8333/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: admin UI --------------------------------------------------- + # Cluster topology, volumes, buckets, maintenance tasks, and Object Store → + # Users, where S3 access keys are minted and revoked. It finds the filer + # through the master, so -master is all it needs. + # + # Bound to loopback by default — the same call SEAWEED_S3_BIND makes above, + # for the same reason: this is storage plumbing, not one of the app's own + # panels. On a remote host that means unreachable, so set + # SEAWEED_ADMIN_BIND=0.0.0.0 and put it behind a reverse proxy. + # + # An unauthenticated panel that can mint credentials for the bucket *is* the + # bucket, so the password is required rather than defaulted — weed leaves auth + # off entirely when it is empty. It is read from WEED_ADMIN_* rather than a + # flag, which keeps it off the process command line. -dataDir persists the + # session key and the maintenance-task settings. + seaweedfs-admin: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-admin + restart: unless-stopped + command: > + admin -port=23646 -master=seaweedfs-master:9333 -dataDir=/data + -metricsPort=9328 + depends_on: + seaweedfs-master: + condition: service_healthy + environment: + WEED_ADMIN_USER: "${SEAWEED_ADMIN_USER:-admin}" + WEED_ADMIN_PASSWORD: "${SEAWEED_ADMIN_PASSWORD:?set SEAWEED_ADMIN_PASSWORD in .env}" + # Optional view-only login. weed ignores it unless the admin password + # above is set, which it is. + WEED_ADMIN_READONLY_USER: "${SEAWEED_ADMIN_READONLY_USER:-}" + WEED_ADMIN_READONLY_PASSWORD: "${SEAWEED_ADMIN_READONLY_PASSWORD:-}" + volumes: + # Its own small volume: session key and maintenance state, no object data. + - "${SEAWEED_ADMIN_DATA:-seaweed_admin}:/data" + ports: + - "${SEAWEED_ADMIN_BIND:-127.0.0.1}:${SEAWEED_ADMIN_PORT:-23646}:23646" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:23646/health || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + 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-s3: + 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 gateway's service name: a server-to-server call inside the compose + # network. + PB_S3_ENDPOINT: "http://seaweedfs-s3: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: + # Shared by master, volume and filer — the same layout `weed server -dir` + # writes, so this file and docker-compose.prod.seaweedfs.yml can swap places + # on it. + seaweed_data: + # The admin UI's own session key and maintenance-task state. Small, and no + # part of the object store. + seaweed_admin: diff --git a/Docker-AIO/docker-compose.seaweedfs.split.yml b/Docker-AIO/docker-compose.seaweedfs.split.yml new file mode 100644 index 0000000..38dd908 --- /dev/null +++ b/Docker-AIO/docker-compose.seaweedfs.split.yml @@ -0,0 +1,319 @@ +name: drivervault-aio + +# Single all-in-one container, with SeaweedFS split into its four roles: +# PocketBase + API Server + Web App (nginx) in one image, beside master, volume, +# filer, S3 gateway and the SeaweedFS admin UI as separate containers. +# Self-contained — one file, nothing to layer. +# +# cp .env.seaweedfs.split.example .env (then edit it) +# docker compose -f docker-compose.seaweedfs.split.yml up -d --build +# +# The build context is the project root so the Dockerfile can reach both +# "API Server/" and "Web App/". +# +# This is docker-compose.seaweedfs.yml with the storage layer taken apart. +# `weed server -s3` runs master, volume, filer and gateway as goroutines in one +# process; here each is its own container. What that buys: +# +# • the admin UI (weed admin) — a cluster view, and Object Store → Users, +# where S3 identities are created and revoked without touching a file; +# • per-role restart, upgrade and Prometheus metrics; +# • room to add a second volume server later, on this host or another. +# +# What it costs: five containers beside the all-in-one instead of one, and five +# healthchecks to keep the boot order honest. If none of the above is wanted, +# use docker-compose.seaweedfs.yml — the S3 behaviour is identical. +# +# None of them run inside the all-in-one image, for the same reason the single +# gateway does not: keeping the object store in that image, on the volume the +# files are being moved off, would defeat the point and would mean rebuilding. +# +# The on-disk layout is deliberately the same as the single-process file's: +# master, volume and filer share one /data mount, exactly as `weed server -dir` +# lays it out (master raft state, volume .dat/.idx, the filer's filerldb2/ — no +# filename overlap). So the two files are interchangeable on the same volume, +# with no migration either way. A *second* volume server would need its own. +# +# Before turning this on for a stack that already has uploads: PocketBase does +# NOT copy existing files into the bucket. See README.md. + +services: + # --- SeaweedFS: master ----------------------------------------------------- + # Keeps the volume/topology metadata and hands out file ids. -ip is the name + # the other roles are told to reach it by, so it must be the service name and + # not the container IP the process would otherwise detect. + seaweedfs-master: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-master + restart: unless-stopped + command: > + master -ip=seaweedfs-master -ip.bind=0.0.0.0 -mdir=/data + -volumeSizeLimitMB=1024 -metricsPort=9324 + volumes: + - seaweed_data:/data + ports: + # Master UI / API. Useful while developing; the admin UI below covers the + # same ground with a nicer face. + - "${SEAWEED_MASTER_PORT:-9333}:9333" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: volume server --------------------------------------------- + # Where the bytes actually land. -max=0 lets it size itself from free disk + # rather than the default cap of 8 volumes. + seaweedfs-volume: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-volume + restart: unless-stopped + command: > + volume -master=seaweedfs-master:9333 -ip=seaweedfs-volume -ip.bind=0.0.0.0 + -port=8080 -dir=/data -max=0 -metricsPort=9325 + depends_on: + seaweedfs-master: + condition: service_healthy + volumes: + - seaweed_data:/data + ports: + # This port serves file content by file id with NO authentication — the S3 + # credentials do not apply to it. Publish it only where you would be + # willing to publish the bucket itself. + - "${SEAWEED_VOLUME_PORT:-8081}:8080" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: filer ------------------------------------------------------ + # Gives the flat volume store a directory tree — buckets, object keys — and + # holds the S3 identities the admin UI writes. -defaultStoreDir is where its + # embedded leveldb goes; without it that would be the container's working + # directory, and the identities would not survive a recreate. + seaweedfs-filer: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-filer + restart: unless-stopped + command: > + filer -master=seaweedfs-master:9333 -ip=seaweedfs-filer -ip.bind=0.0.0.0 + -port=8888 -defaultStoreDir=/data -metricsPort=9326 + depends_on: + seaweedfs-volume: + condition: service_healthy + volumes: + - seaweed_data:/data + ports: + - "${SEAWEED_FILER_PORT:-8888}:8888" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8888/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: bucket and identity seed ---------------------------------- + # Runs once and exits, before the gateway starts. Two jobs: + # + # 1. create the bucket — PocketBase never issues a CreateBucket of its own; + # 2. write PocketBase's S3 identity into the filer's IAM store. + # + # (2) is why this stack does not set AWS_ACCESS_KEY_ID on the gateway, the way + # docker-compose.seaweedfs.yml does. Those env vars are the *lowest* priority + # credential source in SeaweedFS: they are read only while the filer's store is + # empty, so the first identity added in the admin UI would silently displace + # them and lock PocketBase out. Seeding the store the admin UI itself writes + # leaves one source of truth, and the key PocketBase uses appears under + # Object Store → Users like any other. + # + # Both commands update in place, so every later boot re-applies the values from + # .env and changes nothing else — which is also how a rotated PB_S3_SECRET + # reaches the gateway. + # + # The closing grep is the gate: an empty IAM store means the gateway would come + # up in its allow-anyone default, so this fails loudly instead and the gateway + # below never starts. No `|| true` here, deliberately. + seaweedfs-init: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-init + restart: "no" + depends_on: + seaweedfs-filer: + condition: service_healthy + environment: + # Passed as env and expanded by the shell inside the container, so the + # secret stays out of the container's argv. + 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}" + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + printf '%s\n' \ + "s3.bucket.create -name $$PB_S3_BUCKET" \ + "s3.configure -user drivervault -access_key $$PB_S3_ACCESS_KEY -secret_key $$PB_S3_SECRET -actions Admin -apply" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 + echo "s3.configure" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 \ + | grep -q "$$PB_S3_ACCESS_KEY" + + # --- SeaweedFS: S3 gateway ------------------------------------------------- + # The endpoint PocketBase talks to. No -config file: with only -filer given, + # credentials come from the filer's IAM store, which is what lets the admin UI + # add and revoke identities without a restart. A config file would take + # priority over that store and make the admin UI's users inert. + seaweedfs-s3: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-s3 + restart: unless-stopped + command: > + s3 -filer=seaweedfs-filer:8888 -ip.bind=0.0.0.0 -port=8333 + -metricsPort=9327 + depends_on: + seaweedfs-filer: + condition: service_healthy + # Never serve before an identity exists — see seaweedfs-init above. + seaweedfs-init: + condition: service_completed_successfully + ports: + # The stack reaches the gateway over the compose network; this is here so + # `aws s3 ls --endpoint-url http://localhost:8333` works while developing. + - "${SEAWEED_S3_PORT:-8333}:8333" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8333/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: admin UI --------------------------------------------------- + # http://localhost:23646 — cluster topology, volumes, buckets, maintenance + # tasks, and Object Store → Users, where S3 access keys are minted and revoked. + # It finds the filer through the master, so -master is all it needs. + # + # An unauthenticated panel that can mint credentials for the bucket *is* the + # bucket, so the password is required rather than defaulted — weed leaves auth + # off entirely when it is empty. It is read from WEED_ADMIN_* rather than a + # flag, which keeps it off the process command line. -dataDir persists the + # session key and the maintenance-task settings. + seaweedfs-admin: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-aio-seaweedfs-admin + restart: unless-stopped + command: > + admin -port=23646 -master=seaweedfs-master:9333 -dataDir=/data + -metricsPort=9328 + depends_on: + seaweedfs-master: + condition: service_healthy + environment: + WEED_ADMIN_USER: "${SEAWEED_ADMIN_USER:-admin}" + WEED_ADMIN_PASSWORD: "${SEAWEED_ADMIN_PASSWORD:?set SEAWEED_ADMIN_PASSWORD in .env}" + volumes: + - seaweed_admin:/data + ports: + - "${SEAWEED_ADMIN_PORT:-23646}:23646" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:23646/health || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + drivervault: + build: + # Project root (one level up from this compose file). + context: .. + dockerfile: Docker-AIO/Dockerfile + args: + # Empty -> bundle uses same-origin "/api", proxied internally by nginx. + - VITE_API_BASE=${VITE_API_BASE:-} + # Bare name = pass through only when set in the environment, so an unset + # PB_VERSION leaves the Dockerfile pin in place instead of overriding it + # with an empty string (which would resolve "latest" at build time). + - PB_VERSION + image: drivervault-aio + 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-s3: + 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 gateway's service name: a server-to-server call inside the compose + # network. + PB_S3_ENDPOINT: "http://seaweedfs-s3: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: the API Server keeps no state on disk, so everything + # it owns — plugin settings included — lives in the database. + - 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: + # Shared by master, volume and filer — the same layout `weed server -dir` + # writes, so this file and docker-compose.seaweedfs.yml can swap places on it. + seaweed_data: + # The admin UI's own session key and maintenance-task state. Small, and no + # part of the object store. + seaweed_admin: diff --git a/Docker/.env.prod.seaweedfs.split.example b/Docker/.env.prod.seaweedfs.split.example new file mode 100644 index 0000000..01c9d0d --- /dev/null +++ b/Docker/.env.prod.seaweedfs.split.example @@ -0,0 +1,166 @@ +# DriverVault — production stack config, SeaweedFS split into its four roles. +# Copy to .env and fill in, then: +# docker compose -f docker-compose.prod.seaweedfs.split.yml pull +# docker compose -f docker-compose.prod.seaweedfs.split.yml up -d + +# --- Registry images --------------------------------------------------------- +# Defaults point at the internal registry; override to pin a tag or use a mirror. +PB_IMAGE=10.2.1.10:5500/admin/drivervault-pocketbase:latest +API_IMAGE=10.2.1.10:5500/admin/drivervault-api-server:latest +WEB_IMAGE=10.2.1.10:5500/admin/drivervault-web-app:latest + +# --- PocketBase superuser ---------------------------------------------------- +# Created/updated on the PocketBase container's first boot. The API Server uses +# these same credentials to manage the database. REQUIRED. +PB_ADMIN_EMAIL=admin@example.com +PB_ADMIN_PASSWORD=change-me-long-password + +# --- DriverVault super-admin (app login) ------------------------------------- +# The first application user, created by the API Server on boot with role +# "superadmin" if no user with this email exists yet. Leave blank to skip and +# create the first user by hand. This is the account you log in to the web app +# with — distinct from the PocketBase superuser above. +DRIVERVAULT_SUPERADMIN_EMAIL=owner@example.com +DRIVERVAULT_SUPERADMIN_PASSWORD=change-me-long-password +DRIVERVAULT_SUPERADMIN_NAME=Administrator + +# Schema creation/reconcile on boot. Leave this true: a release can add +# collections or fields the server needs, and a stack that skips the bootstrap +# never gets them. (The API Server creates app_settings, which holds the plugin +# settings, on demand — but only that one.) Set false only for a database you +# know already matches the release. +PB_BOOTSTRAP=true + +# --- API Server -------------------------------------------------------------- +# Allowed CORS origin(s) for the web app (match your public URL / WEB_PORT). +CORS_ALLOW_ORIGINS=http://localhost:8090 +AUTH_USERS_COLLECTION=users + +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. Keep the default and terminate +# TLS in a reverse proxy in front of this stack, setting OCPP_PUBLIC_URL to the +# public wss:// base the charger should be pointed at (deriving it from request +# headers is unreliable behind a proxy). Turning the check off is for trusted +# networks only. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + +# Diagnostic only. Set to 1 to log every frame the charger publishes over Anker's +# cloud broker — the ones DriverVault decodes and the ones it cannot, with their +# bytes. It is how an unnamed frame gets named: hold a control read open, do the +# thing in the Anker app, then read the frames back out of the container log. +# Leave blank on a normal stack; a triggered charger writes a line every few +# seconds. +ANKER_MQTT_FRAME_LOG= + +# The charger can dial either door: the API Server port directly, or the Web +# App port, whose BFF now proxies /ocpp/ through to it. The endpoint the panel +# shows is the API Server port only when OCPP_PUBLIC_URL says so — left blank it +# is whichever host the panel itself was reached on, which is the Web App. +# TRUST_FORWARDED_PROTO lets the BFF pass an inbound X-Forwarded-Proto to the +# API Server: needed when TLS ends at a proxy in front of the stack and +# OCPP_REQUIRE_TLS stays on, and unsafe otherwise, since the header is then +# whatever the client said it was. +TRUST_FORWARDED_PROTO=false + +# --- Ports ------------------------------------------------------------------- +# WEB_PORT is the public front door (bound on all interfaces). +WEB_PORT=8090 +# PocketBase admin UI and the API panel are bound to localhost only by default. +# Set PB_BIND / API_BIND to 0.0.0.0 to expose them on the network — which is +# what a stack running on a remote host needs, behind a reverse proxy. +PB_PORT=8070 +PB_BIND=127.0.0.1 +API_PORT=8080 +API_BIND=127.0.0.1 + +# --- Settings the API Server panel can also change --------------------------- +# The panel's Settings screens apply these immediately, but only for the life of +# the container — the values below are re-applied on every restart and win. Set +# them here to make a change permanent. +# POCKETBASE_URL where the API Server looks for the database. Defaults to +# the bundled pocketbase service; set it to reach one +# outside this stack. +# WEBAPP_URL the Web App address the panel status page probes. It is +# a container-to-container call, so it must be reachable +# from the API Server, not from your browser. +# CORS_ALLOW_ORIGINS browser origins allowed to call the API Server directly. +# POCKETBASE_URL=http://pocketbase:8070 +# WEBAPP_URL=http://web-app:8090 + +# --- Storage ----------------------------------------------------------------- +# One Docker-managed named volume by default. To store it on a host path +# instead, set an absolute path, e.g. PB_DATA=/srv/drivervault/pb_data. +# PB_DATA — the PocketBase database and its backups. The API Server keeps no +# state on disk, so everything it owns (plugin settings included) is backed up +# by backing up this one path — together with SEAWEED_DATA below. +PB_DATA=pb_data + +# --- File storage: SeaweedFS ------------------------------------------------- +# PocketBase keeps its record files — document scans, service and refill +# receipts, workshop invoices, part photos — in the bucket below instead of on +# PB_DATA. The database and PocketBase's own backups stay where they are. +# +# The credentials do double duty: seaweedfs-init writes them into the filer's +# IAM store as the identity named "drivervault" *and* they are what PocketBase +# authenticates with. There are no safe defaults, and the stack refuses to start +# without them. Change them here and restart to rotate: the seed updates the +# identity in place rather than adding a second one. +PB_S3_ACCESS_KEY= +PB_S3_SECRET= +# The bucket. Created on first boot by the seaweedfs-init container. +PB_S3_BUCKET=drivervault +# SeaweedFS ignores the region; PocketBase insists on having one. +PB_S3_REGION=us-east-1 + +# SEAWEED_DATA — where SeaweedFS keeps the files. A Docker-managed named volume +# by default; set an absolute host path for a bind mount, the same way PB_DATA +# works above. Back it up alongside PB_DATA: from here on the attachments live +# here, not in the database volume. +# +# The master, volume and filer containers all mount it at /data, which is the +# layout `weed server -dir=/data` writes — so this file and +# docker-compose.prod.seaweedfs.yml are interchangeable on the same volume, with +# nothing to migrate either way. +SEAWEED_DATA=seaweed_data +# The SeaweedFS image, pinned so a redeploy months from now brings up the same +# one. All five SeaweedFS containers run it. +# SEAWEED_IMAGE=chrislusf/seaweedfs:4.45 +# The S3 port is published on loopback only — the stack reaches the gateway over +# the compose network, and this is for tools like aws-cli. Set +# SEAWEED_S3_BIND=0.0.0.0 to expose it to other hosts, and mean it. +# SEAWEED_S3_BIND=127.0.0.1 +# SEAWEED_S3_PORT=8333 +# +# The master, volume and filer publish no host port at all. The admin UI below +# shows what they would: the volume server in particular serves file content by +# id with no authentication, so it stays on the compose network. Reach the +# others with `docker compose exec`. + +# --- SeaweedFS admin UI ------------------------------------------------------ +# Cluster topology, volumes, buckets, maintenance tasks, and Object Store → +# Users, where further S3 identities are created and revoked. They land in the +# filer's IAM store, the same one seeded above, and the gateway picks them up +# without a restart. +# +# REQUIRED: weed disables authentication entirely when the password is empty, +# and this panel can mint credentials for the bucket. +SEAWEED_ADMIN_USER=admin +SEAWEED_ADMIN_PASSWORD= +# Optional view-only login. +SEAWEED_ADMIN_READONLY_USER= +SEAWEED_ADMIN_READONLY_PASSWORD= +# Bound to localhost by default, like PB_BIND and API_BIND. On a remote host +# that means unreachable — set 0.0.0.0 and put it behind the same reverse proxy +# as the other panels. +SEAWEED_ADMIN_BIND=127.0.0.1 +SEAWEED_ADMIN_PORT=23646 +# Its own small volume: session key and maintenance-task state, no object data. +SEAWEED_ADMIN_DATA=seaweed_admin + +# Existing uploads are NOT migrated when this is switched on: PocketBase copies +# nothing, so attachments made before the switch stop resolving. Read the file +# storage section of README.md first. diff --git a/Docker/.env.seaweedfs.split.example b/Docker/.env.seaweedfs.split.example new file mode 100644 index 0000000..3f3b218 --- /dev/null +++ b/Docker/.env.seaweedfs.split.example @@ -0,0 +1,126 @@ +# Copy to .env and fill in. Used by docker-compose.seaweedfs.split.yml — the +# same stack as .env.seaweedfs.example, with SeaweedFS running as separate +# master / volume / filer / S3 containers plus the SeaweedFS admin UI. + +# --- PocketBase superuser (also used by the API Server to authenticate) ------ +PB_ADMIN_EMAIL=admin@example.com +PB_ADMIN_PASSWORD=change-me-long-password + +# --- DriverVault super-admin (app login) ------------------------------------- +# The first application user, created by the API Server on boot with role +# "superadmin" if no user with this email exists yet. Leave these blank and the +# schema is still created but no user is, leaving a stack you cannot log into. +DRIVERVAULT_SUPERADMIN_EMAIL=owner@example.com +DRIVERVAULT_SUPERADMIN_PASSWORD=change-me-long-password +DRIVERVAULT_SUPERADMIN_NAME=Administrator + +# Schema creation/reconcile on boot. Leave this true: a release can add +# collections or fields the server needs, and a stack that skips the bootstrap +# never gets them. (The API Server creates app_settings, which holds the plugin +# settings, on demand — but only that one.) Set false only for a database you +# know already matches the release. +PB_BOOTSTRAP=true + +# --- API Server ------------------------------------------------------------- +# Allowed CORS origin(s) for the web app (match WEB_PORT / your public URL). +# Native mobile apps are not subject to CORS. +CORS_ALLOW_ORIGINS=http://localhost:8090 +AUTH_USERS_COLLECTION=users + +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. This dev stack serves plain +# HTTP: either terminate TLS in front of it and set OCPP_PUBLIC_URL to the +# public wss:// base, or set OCPP_REQUIRE_TLS=false on a trusted network. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + +# Diagnostic only. Set to 1 to log every frame the charger publishes over Anker's +# cloud broker — the ones DriverVault decodes and the ones it cannot, with their +# bytes. It is how an unnamed frame gets named: hold a control read open, do the +# thing in the Anker app, then read the frames back out of the container log. +# Leave blank on a normal stack; a triggered charger writes a line every few +# seconds. +ANKER_MQTT_FRAME_LOG= + +# The charger can dial either door: the API Server port directly, or the Web +# App port, whose BFF now proxies /ocpp/ through to it. The endpoint the panel +# shows is the API Server port only when OCPP_PUBLIC_URL says so — left blank it +# is whichever host the panel itself was reached on, which is the Web App. +# TRUST_FORWARDED_PROTO lets the BFF pass an inbound X-Forwarded-Proto to the +# API Server: needed when TLS ends at a proxy in front of the stack and +# OCPP_REQUIRE_TLS stays on, and unsafe otherwise, since the header is then +# whatever the client said it was. +TRUST_FORWARDED_PROTO=false + +# --- Host port mappings (optional; defaults shown) -------------------------- +PB_PORT=8070 +API_PORT=8080 +WEB_PORT=8090 + +# --- Web App build ----------------------------------------------------------- +# Leave empty so the browser uses same-origin /api (proxied by the BFF). +VITE_API_BASE= + +# --- Settings the API Server panel can also change --------------------------- +# The panel's Settings screens apply these immediately, but only for the life of +# the container — the values below are re-applied on every restart and win. Set +# them here to make a change permanent. +# POCKETBASE_URL where the API Server looks for the database. Defaults to +# the bundled pocketbase service; set it to reach one +# outside this stack. +# WEBAPP_URL the Web App address the panel status page probes. It is +# a container-to-container call, so it must be reachable +# from the API Server, not from your browser. +# CORS_ALLOW_ORIGINS browser origins allowed to call the API Server directly. +# POCKETBASE_URL=http://pocketbase:8070 +# WEBAPP_URL=http://web-app:8090 + +# --- File storage: SeaweedFS ------------------------------------------------- +# PocketBase keeps its record files — document scans, service and refill +# receipts, workshop invoices, part photos — in the bucket below instead of on +# pb_data. The database and PocketBase's own backups stay where they are. +# +# The credentials do double duty: seaweedfs-init writes them into the filer's +# IAM store as the identity named "drivervault" *and* they are what PocketBase +# authenticates with. There are no safe defaults, and the stack refuses to start +# without them. Change them here and restart to rotate: the seed updates the +# identity in place rather than adding a second one. +PB_S3_ACCESS_KEY= +PB_S3_SECRET= +# The bucket. Created on first boot by the seaweedfs-init container. +PB_S3_BUCKET=drivervault +# SeaweedFS ignores the region; PocketBase insists on having one. +PB_S3_REGION=us-east-1 +# The SeaweedFS image, pinned so a rebuild months from now brings up the same +# one. All five SeaweedFS containers run it. +# SEAWEED_IMAGE=chrislusf/seaweedfs:4.45 + +# --- SeaweedFS admin UI ------------------------------------------------------ +# http://localhost:23646 — cluster topology, volumes, buckets, and +# Object Store → Users, where further S3 identities are created and revoked. +# They land in the filer's IAM store, the same one seeded above, and the gateway +# picks them up without a restart. +# +# REQUIRED: weed disables authentication entirely when the password is empty, +# and this panel can mint credentials for the bucket. +SEAWEED_ADMIN_USER=admin +SEAWEED_ADMIN_PASSWORD= +# SEAWEED_ADMIN_PORT=23646 + +# --- SeaweedFS host ports (optional; defaults shown) ------------------------- +# Published for aws-cli, `weed shell` and poking around while developing. The +# stack itself reaches every one of these over the compose network. +# Note SEAWEED_VOLUME_PORT: the volume server serves file content by id with NO +# authentication, so do not carry this mapping over to a shared machine. It +# lands on 8081 because API_PORT already has 8080. +# SEAWEED_MASTER_PORT=9333 +# SEAWEED_VOLUME_PORT=8081 +# SEAWEED_FILER_PORT=8888 +# SEAWEED_S3_PORT=8333 + +# Existing uploads are NOT migrated when this is switched on: PocketBase copies +# nothing, so attachments made before the switch stop resolving. Read the file +# storage section of README.md first. diff --git a/Docker/README.md b/Docker/README.md index 4f15f03..357cf79 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -95,6 +95,7 @@ remember — with an `.env` example of the same name: |---|---|---| | **Local storage** — the default, unchanged | `docker-compose.prod.yml` | `docker-compose.yml` | | **SeaweedFS in this stack** | `docker-compose.prod.seaweedfs.yml` | `docker-compose.seaweedfs.yml` | +| **SeaweedFS, split into its roles** | `docker-compose.prod.seaweedfs.split.yml` | `docker-compose.seaweedfs.split.yml` | | **An S3 endpoint outside it** | `docker-compose.prod.s3.yml` | `docker-compose.s3.yml` | So `docker-compose.prod.seaweedfs.yml` is configured from @@ -114,6 +115,39 @@ plus a one-shot `seaweedfs-init` that creates the bucket, because PocketBase nev issues a `CreateBucket` of its own. The external-S3 ones add no containers at all: set `PB_S3_ENDPOINT`, and create the bucket yourself. +### Split SeaweedFS + +`weed server -s3` runs master, volume, filer and gateway as four goroutines in +one process. The `.split.` files run them as four containers, plus a fifth: the +SeaweedFS **admin UI** on port 23646, where the cluster can be inspected and — +under *Object Store → Users* — further S3 identities minted and revoked. Split +also gets you per-role restarts and upgrades, per-role Prometheus metrics, and +room to add a second volume server later. + +Identities work differently there, and it matters. SeaweedFS reads credentials +from, in descending priority: an `-s3.config` file, the filer's IAM store, then +`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` — and a higher source *replaces* +a lower one rather than adding to it. The single-process files use the env vars, +which is why nothing else may write identities there: the first user added in a +panel would displace PocketBase's key. So in the split files `seaweedfs-init` +seeds PocketBase's identity into the filer's store instead — the same store the +admin UI writes — and the gateway runs with no config file at all. One source of +truth, PocketBase's key visible in the panel beside every other, and new keys +picked up without a restart. Rotating `PB_S3_SECRET` in `.env` and restarting +updates that identity in place. + +Set `SEAWEED_ADMIN_PASSWORD`: `weed admin` serves the panel with no +authentication when it is empty, and a panel that can mint bucket credentials is +the bucket. In the prod file it is bound to loopback like `PB_BIND` and +`API_BIND`, so a remote host needs `SEAWEED_ADMIN_BIND=0.0.0.0` behind the same +reverse proxy. That file publishes nothing for master, volume and filer — the +volume server serves file content by id with no authentication of any kind, and +the admin UI already shows what those ports would. + +Switching between `docker-compose.seaweedfs.yml` and its `.split.` twin needs no +migration: master, volume and filer share one `/data` mount, which is exactly +the layout `weed server -dir=/data` writes. + On every boot the API Server's bootstrap writes PocketBase's *Files storage* settings from those variables, then asks PocketBase to prove it can reach the bucket. Watch for it in the log: diff --git a/Docker/docker-compose.prod.seaweedfs.split.yml b/Docker/docker-compose.prod.seaweedfs.split.yml new file mode 100644 index 0000000..b4642f2 --- /dev/null +++ b/Docker/docker-compose.prod.seaweedfs.split.yml @@ -0,0 +1,394 @@ +name: drivervault + +# Production DriverVault stack, with SeaweedFS split into its four roles — +# pulls prebuilt images from the registry instead of building from source. +# Self-contained: one file, no overlays. Everything an operator needs to set +# lives in .env. +# +# 1. cp .env.prod.seaweedfs.split.example .env (then edit it) +# 2. docker compose -f docker-compose.prod.seaweedfs.split.yml pull +# 3. docker compose -f docker-compose.prod.seaweedfs.split.yml up -d +# +# This is docker-compose.prod.seaweedfs.yml with the storage layer taken apart. +# `weed server -s3` runs master, volume, filer and gateway as goroutines in one +# process; here each is its own container, plus the SeaweedFS admin UI. What +# that buys: +# +# • the admin UI (weed admin) — a cluster view, and Object Store → Users, +# where S3 identities are created and revoked without touching a file; +# • per-role restart, upgrade and Prometheus metrics; +# • room to add a second volume server later, on this host or another. +# +# What it costs: five containers instead of one, five healthchecks to keep the +# boot order honest, and one more port worth binding carefully. If none of the +# above is wanted, use docker-compose.prod.seaweedfs.yml — the S3 behaviour is +# identical. +# +# The on-disk layout is deliberately the same as the single-process file's: +# master, volume and filer share one /data mount, exactly as `weed server -dir` +# lays it out (master raft state, volume .dat/.idx, the filer's filerldb2/ — no +# filename overlap). So the two files are interchangeable on the same SEAWEED_DATA, +# with no migration either way. A *second* volume server would need its own. +# +# Only the S3 gateway and the admin UI publish a host port. Master, volume and +# filer are reachable over the compose network, through the admin UI, or with +# `docker compose exec` — the volume server in particular serves file content by +# id with no authentication at all, so it has no business on a public interface. +# +# Before turning this on for a stack that already has uploads: PocketBase does +# NOT copy existing files into the bucket. See README.md. +# +# 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: + # --- SeaweedFS: master ----------------------------------------------------- + # Keeps the volume/topology metadata and hands out file ids. -ip is the name + # the other roles are told to reach it by, so it must be the service name and + # not the container IP the process would otherwise detect. + seaweedfs-master: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-master + restart: unless-stopped + command: > + master -ip=seaweedfs-master -ip.bind=0.0.0.0 -mdir=/data + -volumeSizeLimitMB=1024 -metricsPort=9324 + 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" + # No published port: the master UI is one of the pages the admin UI serves. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: volume server --------------------------------------------- + # Where the bytes actually land. -max=0 lets it size itself from free disk + # rather than the default cap of 8 volumes. + seaweedfs-volume: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-volume + restart: unless-stopped + command: > + volume -master=seaweedfs-master:9333 -ip=seaweedfs-volume -ip.bind=0.0.0.0 + -port=8080 -dir=/data -max=0 -metricsPort=9325 + depends_on: + seaweedfs-master: + condition: service_healthy + volumes: + - "${SEAWEED_DATA:-seaweed_data}:/data" + # No published port, and this one is not an oversight: 8080 serves file + # content by file id with NO authentication — the S3 credentials do not + # apply to it. Publishing it would publish every attachment in the stack. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: filer ------------------------------------------------------ + # Gives the flat volume store a directory tree — buckets, object keys — and + # holds the S3 identities the admin UI writes. -defaultStoreDir is where its + # embedded leveldb goes; without it that would be the container's working + # directory, and the identities would not survive a recreate. + seaweedfs-filer: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-filer + restart: unless-stopped + command: > + filer -master=seaweedfs-master:9333 -ip=seaweedfs-filer -ip.bind=0.0.0.0 + -port=8888 -defaultStoreDir=/data -metricsPort=9326 + depends_on: + seaweedfs-volume: + condition: service_healthy + volumes: + - "${SEAWEED_DATA:-seaweed_data}:/data" + # No published port. The filer's gRPC side (8888 + 10000) carries the IAM + # service that mints S3 credentials; keep both ends of it on the compose + # network. + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8888/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: bucket and identity seed ---------------------------------- + # Runs once and exits, before the gateway starts. Two jobs: + # + # 1. create the bucket — PocketBase never issues a CreateBucket of its own; + # 2. write PocketBase's S3 identity into the filer's IAM store. + # + # (2) is why this stack does not set AWS_ACCESS_KEY_ID on the gateway, the way + # docker-compose.prod.seaweedfs.yml does. Those env vars are the *lowest* + # priority credential source in SeaweedFS: they are read only while the filer's + # store is empty, so the first identity added in the admin UI would silently + # displace them and lock PocketBase out. Seeding the store the admin UI itself + # writes leaves one source of truth, and the key PocketBase uses appears under + # Object Store → Users like any other. + # + # Both commands update in place, so every later boot re-applies the values from + # .env and changes nothing else — which is also how a rotated PB_S3_SECRET + # reaches the gateway. + # + # The closing grep is the gate: an empty IAM store means the gateway would come + # up in its allow-anyone default, so this fails loudly instead and the gateway + # below never starts. No `|| true` here, deliberately. + seaweedfs-init: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-init + restart: "no" + depends_on: + seaweedfs-filer: + condition: service_healthy + environment: + # Passed as env and expanded by the shell inside the container, so the + # secret stays out of the container's argv. + 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}" + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + printf '%s\n' \ + "s3.bucket.create -name $$PB_S3_BUCKET" \ + "s3.configure -user drivervault -access_key $$PB_S3_ACCESS_KEY -secret_key $$PB_S3_SECRET -actions Admin -apply" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 + echo "s3.configure" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 \ + | grep -q "$$PB_S3_ACCESS_KEY" + + # --- SeaweedFS: S3 gateway ------------------------------------------------- + # The endpoint PocketBase talks to. No -config file: with only -filer given, + # credentials come from the filer's IAM store, which is what lets the admin UI + # add and revoke identities without a restart. A config file would take + # priority over that store and make the admin UI's users inert. + seaweedfs-s3: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-s3 + restart: unless-stopped + command: > + s3 -filer=seaweedfs-filer:8888 -ip.bind=0.0.0.0 -port=8333 + -metricsPort=9327 + depends_on: + seaweedfs-filer: + condition: service_healthy + # Never serve before an identity exists — see seaweedfs-init above. + seaweedfs-init: + condition: service_completed_successfully + 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:8333/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: admin UI --------------------------------------------------- + # Cluster topology, volumes, buckets, maintenance tasks, and Object Store → + # Users, where S3 access keys are minted and revoked. It finds the filer + # through the master, so -master is all it needs. + # + # Bound to loopback by default, like the PocketBase and API panels: on a remote + # host that means unreachable, so set SEAWEED_ADMIN_BIND=0.0.0.0 the same way + # PB_BIND and API_BIND get set — and put it behind the same reverse proxy. + # + # An unauthenticated panel that can mint credentials for the bucket *is* the + # bucket, so the password is required rather than defaulted — weed leaves auth + # off entirely when it is empty. It is read from WEED_ADMIN_* rather than a + # flag, which keeps it off the process command line. -dataDir persists the + # session key and the maintenance-task settings. + seaweedfs-admin: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-admin + restart: unless-stopped + command: > + admin -port=23646 -master=seaweedfs-master:9333 -dataDir=/data + -metricsPort=9328 + depends_on: + seaweedfs-master: + condition: service_healthy + environment: + WEED_ADMIN_USER: "${SEAWEED_ADMIN_USER:-admin}" + WEED_ADMIN_PASSWORD: "${SEAWEED_ADMIN_PASSWORD:?set SEAWEED_ADMIN_PASSWORD in .env}" + # Optional view-only login. weed ignores it unless the admin password + # above is set, which it is. + WEED_ADMIN_READONLY_USER: "${SEAWEED_ADMIN_READONLY_USER:-}" + WEED_ADMIN_READONLY_PASSWORD: "${SEAWEED_ADMIN_READONLY_PASSWORD:-}" + volumes: + # Its own small volume: session key and maintenance state, no object data. + - "${SEAWEED_ADMIN_DATA:-seaweed_admin}:/data" + ports: + - "${SEAWEED_ADMIN_BIND:-127.0.0.1}:${SEAWEED_ADMIN_PORT:-23646}:23646" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:23646/health || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + pocketbase: + image: "${PB_IMAGE:-10.2.1.10:5500/admin/drivervault-pocketbase:latest}" + container_name: drivervault-pocketbase + restart: unless-stopped + depends_on: + # PocketBase is the process that reads and writes the objects, so the + # gateway has to be serving before it is asked to store anything. + seaweedfs-s3: + condition: service_healthy + 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 + # The bucket must exist before the bootstrap points PocketBase at it. + seaweedfs-init: + condition: service_completed_successfully + environment: + API_ADDR: ":8080" + # Reach PocketBase by its service name on the internal network. Override + # POCKETBASE_URL in .env to point the API Server at a database outside + # this stack — that is also how you make a retarget done from the panel + # permanent, since the panel's change lasts only for the container's life. + POCKETBASE_URL: "${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 the default is the service name — plain + # localhost:8090 would resolve to this container itself. 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://web-app:8090}" + CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" + AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" + # 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). 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:-}" + # Diagnostic: set ANKER_MQTT_FRAME_LOG=1 in .env to log every frame the + # charger publishes over Anker's broker, decoded ones and unreadable ones + # alike, with their bytes. It is how a frame nobody has named gets named — + # do something in the Anker app while a control read holds the connection + # open, and read the frames back out of the log. Off by default: with it on + # a charger under a live trigger writes a line every few seconds. + ANKER_MQTT_FRAME_LOG: "${ANKER_MQTT_FRAME_LOG:-}" + # --- File storage -------------------------------------------------- + # Read by the API Server's bootstrap, which 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 gateway's service name: a server-to-server call inside the compose + # network. + PB_S3_ENDPOINT: "http://seaweedfs-s3: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: + # 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" + # No volume: the API Server keeps no state on disk — every setting it owns, + # plugin settings included, lives in PocketBase under PB_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/* — and /ocpp/*, the address chargers are + # told to dial — to the API Server over the internal network. + API_BASE: "http://api-server:8080" + # Believe an inbound X-Forwarded-Proto. The API Server reads it to decide a + # charger arrived over TLS, so leave this off unless a TLS-terminating + # proxy in front of the stack is the only way in: otherwise a charger could + # claim wss over a plaintext connection. Set it to true when TLS ends at + # that proxy and OCPP_PUBLIC_URL names a wss:// base through it. + TRUST_FORWARDED_PROTO: "${TRUST_FORWARDED_PROTO:-false}" + 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: + # Shared by master, volume and filer — the same layout `weed server -dir` + # writes, so this file and docker-compose.prod.seaweedfs.yml can swap places + # on it. + seaweed_data: + # The admin UI's own session key and maintenance-task state. Small, and no + # part of the object store. + seaweed_admin: diff --git a/Docker/docker-compose.seaweedfs.split.yml b/Docker/docker-compose.seaweedfs.split.yml new file mode 100644 index 0000000..d701347 --- /dev/null +++ b/Docker/docker-compose.seaweedfs.split.yml @@ -0,0 +1,379 @@ +name: drivervault + +# Full DriverVault stack, with SeaweedFS split into its four roles: PocketBase +# (database) + API Server + Web App, built from source, plus master, volume, +# filer, S3 gateway and the SeaweedFS admin UI as separate containers. +# Self-contained — one file, nothing to layer. +# +# cp .env.seaweedfs.split.example .env (then edit it) +# docker compose -f docker-compose.seaweedfs.split.yml up -d --build +# +# Traffic flow (browser): Web App BFF --/api--> API Server --> PocketBase. +# +# This is docker-compose.seaweedfs.yml with the storage layer taken apart. +# `weed server -s3` runs master, volume, filer and gateway as goroutines in one +# process; here each is its own container. What that buys: +# +# • the admin UI (weed admin) — a cluster view, and Object Store → Users, +# where S3 identities are created and revoked without touching a file; +# • per-role restart, upgrade and Prometheus metrics; +# • room to add a second volume server later, on this host or another. +# +# What it costs: five containers instead of one, and five healthchecks to keep +# the boot order honest. If none of the above is wanted, use +# docker-compose.seaweedfs.yml — the S3 behaviour is identical. +# +# The on-disk layout is deliberately the same as the single-process file's: +# master, volume and filer share one /data mount, exactly as `weed server -dir` +# lays it out (master raft state, volume .dat/.idx, the filer's filerldb2/ — no +# filename overlap). So the two files are interchangeable on the same volume, +# with no migration either way. A *second* volume server would need its own. +# +# Before turning this on for a stack that already has uploads: PocketBase does +# NOT copy existing files into the bucket. See README.md. + +services: + # --- SeaweedFS: master ----------------------------------------------------- + # Keeps the volume/topology metadata and hands out file ids. -ip is the name + # the other roles are told to reach it by, so it must be the service name and + # not the container IP the process would otherwise detect. + seaweedfs-master: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-master + restart: unless-stopped + command: > + master -ip=seaweedfs-master -ip.bind=0.0.0.0 -mdir=/data + -volumeSizeLimitMB=1024 -metricsPort=9324 + volumes: + - seaweed_data:/data + ports: + # Master UI / API. Useful while developing; the admin UI below covers the + # same ground with a nicer face. + - "${SEAWEED_MASTER_PORT:-9333}:9333" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: volume server --------------------------------------------- + # Where the bytes actually land. -max=0 lets it size itself from free disk + # rather than the default cap of 8 volumes. + seaweedfs-volume: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-volume + restart: unless-stopped + command: > + volume -master=seaweedfs-master:9333 -ip=seaweedfs-volume -ip.bind=0.0.0.0 + -port=8080 -dir=/data -max=0 -metricsPort=9325 + depends_on: + seaweedfs-master: + condition: service_healthy + volumes: + - seaweed_data:/data + ports: + # This port serves file content by file id with NO authentication — the S3 + # credentials do not apply to it. Publish it only where you would be + # willing to publish the bucket itself. Mapped to 8081 on the host because + # 8080 there is the API Server. + - "${SEAWEED_VOLUME_PORT:-8081}:8080" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: filer ------------------------------------------------------ + # Gives the flat volume store a directory tree — buckets, object keys — and + # holds the S3 identities the admin UI writes. -defaultStoreDir is where its + # embedded leveldb goes; without it that would be the container's working + # directory, and the identities would not survive a recreate. + seaweedfs-filer: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-filer + restart: unless-stopped + command: > + filer -master=seaweedfs-master:9333 -ip=seaweedfs-filer -ip.bind=0.0.0.0 + -port=8888 -defaultStoreDir=/data -metricsPort=9326 + depends_on: + seaweedfs-volume: + condition: service_healthy + volumes: + - seaweed_data:/data + ports: + - "${SEAWEED_FILER_PORT:-8888}:8888" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8888/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: bucket and identity seed ---------------------------------- + # Runs once and exits, before the gateway starts. Two jobs: + # + # 1. create the bucket — PocketBase never issues a CreateBucket of its own; + # 2. write PocketBase's S3 identity into the filer's IAM store. + # + # (2) is why this stack does not set AWS_ACCESS_KEY_ID on the gateway, the way + # docker-compose.seaweedfs.yml does. Those env vars are the *lowest* priority + # credential source in SeaweedFS: they are read only while the filer's store is + # empty, so the first identity added in the admin UI would silently displace + # them and lock PocketBase out. Seeding the store the admin UI itself writes + # leaves one source of truth, and the key PocketBase uses appears under + # Object Store → Users like any other. + # + # Both commands update in place, so every later boot re-applies the values from + # .env and changes nothing else — which is also how a rotated PB_S3_SECRET + # reaches the gateway. + # + # The closing grep is the gate: an empty IAM store means the gateway would come + # up in its allow-anyone default, so this fails loudly instead and the gateway + # below never starts. No `|| true` here, deliberately. + seaweedfs-init: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-init + restart: "no" + depends_on: + seaweedfs-filer: + condition: service_healthy + environment: + # Passed as env and expanded by the shell inside the container, so the + # secret stays out of the container's argv. + 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}" + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + printf '%s\n' \ + "s3.bucket.create -name $$PB_S3_BUCKET" \ + "s3.configure -user drivervault -access_key $$PB_S3_ACCESS_KEY -secret_key $$PB_S3_SECRET -actions Admin -apply" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 + echo "s3.configure" \ + | weed shell -master=seaweedfs-master:9333 -filer=seaweedfs-filer:8888 \ + | grep -q "$$PB_S3_ACCESS_KEY" + + # --- SeaweedFS: S3 gateway ------------------------------------------------- + # The endpoint PocketBase talks to. No -config file: with only -filer given, + # credentials come from the filer's IAM store, which is what lets the admin UI + # add and revoke identities without a restart. A config file would take + # priority over that store and make the admin UI's users inert. + seaweedfs-s3: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-s3 + restart: unless-stopped + command: > + s3 -filer=seaweedfs-filer:8888 -ip.bind=0.0.0.0 -port=8333 + -metricsPort=9327 + depends_on: + seaweedfs-filer: + condition: service_healthy + # Never serve before an identity exists — see seaweedfs-init above. + seaweedfs-init: + condition: service_completed_successfully + ports: + # The stack reaches the gateway over the compose network; this is here so + # `aws s3 ls --endpoint-url http://localhost:8333` works while developing. + - "${SEAWEED_S3_PORT:-8333}:8333" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8333/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + # --- SeaweedFS: admin UI --------------------------------------------------- + # http://localhost:23646 — cluster topology, volumes, buckets, maintenance + # tasks, and Object Store → Users, where S3 access keys are minted and revoked. + # It finds the filer through the master, so -master is all it needs. + # + # An unauthenticated panel that can mint credentials for the bucket *is* the + # bucket, so the password is required rather than defaulted — weed leaves auth + # off entirely when it is empty. It is read from WEED_ADMIN_* rather than a + # flag, which keeps it off the process command line. -dataDir persists the + # session key and the maintenance-task settings. + seaweedfs-admin: + image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}" + container_name: drivervault-seaweedfs-admin + restart: unless-stopped + command: > + admin -port=23646 -master=seaweedfs-master:9333 -dataDir=/data + -metricsPort=9328 + depends_on: + seaweedfs-master: + condition: service_healthy + environment: + WEED_ADMIN_USER: "${SEAWEED_ADMIN_USER:-admin}" + WEED_ADMIN_PASSWORD: "${SEAWEED_ADMIN_PASSWORD:?set SEAWEED_ADMIN_PASSWORD in .env}" + volumes: + - seaweed_admin:/data + ports: + - "${SEAWEED_ADMIN_PORT:-23646}:23646" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:23646/health || exit 1"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 15s + + pocketbase: + build: + context: ./pocketbase + image: drivervault-pocketbase + container_name: drivervault-pocketbase + restart: unless-stopped + depends_on: + # PocketBase is the process that reads and writes the objects, so the + # gateway has to be serving before it is asked to store anything. + seaweedfs-s3: + condition: service_healthy + environment: + # Superuser is created/updated on boot so the API Server can authenticate. + 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: + - pb_data:/pb/pb_data + ports: + # Admin UI / API exposed on the host for management (http://host:8070/_/). + - "${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: + build: + context: ../API Server + image: drivervault-api + container_name: drivervault-api + restart: unless-stopped + depends_on: + pocketbase: + condition: service_healthy + # The bucket must exist before the bootstrap points PocketBase at it. + seaweedfs-init: + condition: service_completed_successfully + environment: + API_ADDR: ":8080" + # Reach PocketBase by its service name on the internal network. Override + # POCKETBASE_URL in .env to point the API Server at a database outside + # this stack — that is also how you make a retarget done from the panel + # permanent, since the panel's change lasts only for the container's life. + POCKETBASE_URL: "${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 the default is the service name — plain + # localhost:8090 would resolve to this container itself. 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://web-app:8090}" + # Same-origin requests go through the Web App BFF, so CORS is only needed + # if the browser ever calls the API Server directly. Default to the web origin. + CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" + AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" + # Schema + super-admin bootstrap (idempotent). Without the SUPERADMIN vars + # the collections are still created but no app user is, leaving a stack + # you cannot log into. + # + # Leave the bootstrap ON: a release can add collections or fields the + # server needs, and a stack that skips it 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 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). Chargers are rejected unless they + # connect over TLS; set OCPP_REQUIRE_TLS=false in .env only when TLS is + # terminated in front of this stack or for local dev on a trusted network. + OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" + OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" + # Diagnostic: set ANKER_MQTT_FRAME_LOG=1 in .env to log every frame the + # charger publishes over Anker's broker, decoded ones and unreadable ones + # alike, with their bytes. It is how a frame nobody has named gets named — + # do something in the Anker app while a control read holds the connection + # open, and read the frames back out of the log. Off by default: with it on + # a charger under a live trigger writes a line every few seconds. + ANKER_MQTT_FRAME_LOG: "${ANKER_MQTT_FRAME_LOG:-}" + # --- File storage -------------------------------------------------- + # Read by the API Server's bootstrap, which 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 gateway's service name: a server-to-server call inside the compose + # network. + PB_S3_ENDPOINT: "http://seaweedfs-s3: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: + # Optional direct access to the API Server (and its panel at /); the Web + # App reaches it over the internal network, not this host port. Chargers + # dialling /ocpp/{serial} also arrive here. + - "${API_PORT:-8080}:8080" + # No volume: the API Server keeps no state on disk — every setting it owns, + # plugin settings included, lives in PocketBase under pb_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: + build: + context: ../Web App + args: + # Empty -> bundle uses same-origin "/api", which the BFF proxies below. + VITE_API_BASE: "${VITE_API_BASE:-}" + image: drivervault-web + 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/* — and /ocpp/*, the address chargers are + # told to dial — to the API Server over the internal network. + API_BASE: "http://api-server:8080" + # Believe an inbound X-Forwarded-Proto. The API Server reads it to decide a + # charger arrived over TLS, so leave this off unless a TLS-terminating + # proxy in front of the stack is the only way in: otherwise a charger could + # claim wss over a plaintext connection. Set it to true when TLS ends at + # that proxy and OCPP_PUBLIC_URL names a wss:// base through it. + TRUST_FORWARDED_PROTO: "${TRUST_FORWARDED_PROTO:-false}" + ports: + - "${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: + # Shared by master, volume and filer — the same layout `weed server -dir` + # writes, so this file and docker-compose.seaweedfs.yml can swap places on it. + seaweed_data: + # The admin UI's own session key and maintenance-task state. Small, and no + # part of the object store. + seaweed_admin: