diff --git a/Docker-AIO/.env.prod.seaweedfs.split.example b/Docker-AIO/.env.prod.seaweedfs.split.example index db150d0..7ef5de8 100644 --- a/Docker-AIO/.env.prod.seaweedfs.split.example +++ b/Docker-AIO/.env.prod.seaweedfs.split.example @@ -1,10 +1,18 @@ -# 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 +# DriverVault all-in-one — production config, SeaweedFS split into its roles and +# built into the same image (Dockerfile.seaweedfs.split). Copy to .env and fill +# in, then: +# docker compose -f docker-compose.prod.seaweedfs.split.yml build # docker compose -f docker-compose.prod.seaweedfs.split.yml up -d +# The build needs the repo checkout; to deploy elsewhere, `push` after building +# and `pull` on the deploy host. -# --- Registry image ---------------------------------------------------------- -AIO_IMAGE=10.2.1.10:5500/admin/drivervault-aio:latest +# --- Image ------------------------------------------------------------------- +# The tag the build produces and `push`/`pull` use. Its own name, not +# drivervault-aio: this image carries SeaweedFS and expects three volumes. +AIO_IMAGE=10.2.1.10:5500/admin/drivervault-aio-seaweedfs-split:latest +# Build args, both pinned in the Dockerfile. Set to override at build time. +# PB_VERSION=0.39.11 +# SEAWEED_VERSION=4.45 # --- PocketBase superuser (required) ----------------------------------------- # Created/updated on first boot. The API Server uses these to manage the database. @@ -67,41 +75,39 @@ PB_DATA=pb_data # 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. +# The credentials do double duty: the S3 gateway's program 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 container +# 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. +# The bucket. Created on first boot by the S3 gateway's program. PB_S3_BUCKET=drivervault -# SeaweedFS ignores the region; PocketBase insists on having one. -PB_S3_REGION=us-east-1 +# The endpoint, region and path style are fixed by the image — the gateway is +# inside the container at a loopback address that cannot change. # 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 +# Master, volume and filer share it (mounted at /seaweed/data), which is the +# layout `weed server -dir` 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 +# The S3 port is published on loopback only — PocketBase reaches the gateway +# inside the container, 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`. +# The master, volume and filer listen on the container's loopback and 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. Reach +# them with `docker compose exec drivervault wget -qO- http://127.0.0.1:9333/...` +# (volume 8081, filer 8888). # --- SeaweedFS admin UI ------------------------------------------------------ # Cluster topology, volumes, buckets, maintenance tasks, and Object Store → @@ -110,7 +116,8 @@ SEAWEED_DATA=seaweed_data # without a restart. # # REQUIRED: weed disables authentication entirely when the password is empty, -# and this panel can mint credentials for the bucket. +# and this panel can mint credentials for the bucket — so the image refuses to +# start the panel at all without one, and the container stays unhealthy. SEAWEED_ADMIN_USER=admin SEAWEED_ADMIN_PASSWORD= # Optional view-only login. diff --git a/Docker-AIO/Dockerfile.seaweedfs.split b/Docker-AIO/Dockerfile.seaweedfs.split new file mode 100644 index 0000000..8f829d0 --- /dev/null +++ b/Docker-AIO/Dockerfile.seaweedfs.split @@ -0,0 +1,462 @@ +# syntax=docker/dockerfile:1 +# +# All-in-one image WITH the object store inside: PocketBase + API Server + Web +# App, plus SeaweedFS split into its roles — master, volume, filer, S3 gateway +# and the admin UI — each its own supervisord program in the same container. +# +# This is Dockerfile with the six SeaweedFS containers of +# docker-compose.prod.seaweedfs.split.yml folded in. The compose split kept +# them outside the image so SeaweedFS could be upgraded without a rebuild; this +# file trades that away for a single image and a single container. What the +# split still buys in here: per-role restart under supervisord, per-role +# metrics ports, and the admin UI, where S3 identities are minted and revoked. +# What it costs: a new SeaweedFS means a rebuild (--build-arg SEAWEED_VERSION). +# +# The build context MUST be the project root so this file can reach both +# "API Server/" and "Web App/". The root .dockerignore is an allow-list of the +# paths copied below — add to it if you add a COPY here. Build it with: +# +# docker build -f "Docker-AIO/Dockerfile.seaweedfs.split" -t drivervault-aio-seaweedfs-split . +# +# Run it (everything starts together): +# +# docker run -d --name drivervault -p 80:80 -p 8070:8070 -p 8080:8080 \ +# -p 127.0.0.1:23646:23646 \ +# -e PB_ADMIN_EMAIL=admin@example.com \ +# -e PB_ADMIN_PASSWORD=change-me \ +# -e PB_S3_ACCESS_KEY=drivervault -e PB_S3_SECRET=change-me \ +# -e WEED_ADMIN_PASSWORD=change-me \ +# -v drivervault_pb:/pb/pb_data \ +# -v drivervault_seaweed:/seaweed/data \ +# -v drivervault_seaweed_admin:/seaweed/admin \ +# drivervault-aio-seaweedfs-split +# +# Then: web app on http://host/, PocketBase admin on http://host:8070/_/, and +# the SeaweedFS admin UI on http://127.0.0.1:23646/. On first boot the S3 +# gateway's program creates the bucket and seeds PocketBase's identity, and the +# API Server creates the collections, the super-admin, and points PocketBase's +# file storage at the bucket. +# +# Port map inside the container (only 80, 8070, 8080, 8333 and 23646 are meant +# to be published; the rest are bound to loopback): +# +# 80 nginx (Web App, /api/ and /ocpp/ proxied) +# 8070 PocketBase +# 8080 API Server ← which is why the volume server is NOT on its +# 8081 SeaweedFS volume usual 8080 here +# 8333 SeaweedFS S3 gateway (PocketBase's endpoint; publish on loopback) +# 8888 SeaweedFS filer +# 9333 SeaweedFS master +# 23646 SeaweedFS admin UI (publish on loopback, behind a proxy if remote) + +# SeaweedFS release to bake in, pinned like PB_VERSION so a rebuild months from +# now brings up the same one. Same tag the compose SeaweedFS files run as +# SEAWEED_IMAGE. Override with --build-arg SEAWEED_VERSION=... to upgrade. +ARG SEAWEED_VERSION="4.45" + +# --- Stage 1: build the Go API Server --------------------------------------- +FROM golang:1.26-alpine3.24 AS api-build +WORKDIR /src +COPY ["API Server/go.mod", "./"] +COPY ["API Server/go.su[m]", "./"] +RUN go mod download +# Only cmd/ + internal are needed; the panel is already built into +# internal/api/dist and embedded via //go:embed. Entry point is cmd/server. +COPY ["API Server/cmd", "./cmd"] +COPY ["API Server/internal", "./internal"] +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api-server ./cmd/server + +# --- Stage 2: build the Vue Web App ----------------------------------------- +# The Vue source lives under "Web App/web/". +FROM node:22-alpine3.24 AS web-build +WORKDIR /app +COPY ["Web App/web/package.json", "Web App/web/package-lock.json", "./"] +RUN npm ci +COPY ["Web App/web/index.html", "Web App/web/vite.config.js", "./"] +COPY ["Web App/web/src", "./src"] +COPY ["Web App/web/public", "./public"] +# Empty -> bundle uses same-origin "/api", proxied to the API Server by nginx. +ARG VITE_API_BASE +# vite.config writes to ../server/dist by default; emit into ./dist here. +RUN npm run build -- --outDir dist --emptyOutDir + +# --- Stage 3: SeaweedFS ----------------------------------------------------- +# The one static binary from the official image. SEAWEED_VERSION is declared at +# the top of the file: an ARG used in a FROM has to be global, and one declared +# here would belong to the stage above and expand to nothing. +FROM chrislusf/seaweedfs:${SEAWEED_VERSION} AS seaweed + +# --- Stage 4: runtime (all services) ---------------------------------------- +FROM alpine:3.24 + +# Pinned so a rebuild months from now produces the same PocketBase. Override to +# upgrade (--build-arg PB_VERSION=0.40.0); set it to empty to resolve the latest +# release at build time, which needs an unauthenticated GitHub API call and is +# therefore subject to that GitHub rate limit (60/hour per IP). +ARG PB_VERSION="0.39.11" +# Provided automatically by BuildKit (amd64 / arm64). +ARG TARGETARCH="amd64" + +RUN apk add --no-cache ca-certificates tzdata unzip wget nginx supervisor \ + && mkdir -p /run/nginx + +# Unprivileged account for PocketBase, the API Server and every SeaweedFS role. +# Only nginx stays root, because it binds port 80; supervisord drops to this +# user for everything else. +RUN addgroup -S app && adduser -S -G app app + +# PocketBase from the official release (pinned via PB_VERSION, else latest). +WORKDIR /pb +RUN set -eux; \ + ver="${PB_VERSION}"; \ + if [ -z "$ver" ]; then \ + ver="$(wget -qO- https://api.github.com/repos/pocketbase/pocketbase/releases/latest \ + | grep -o '"tag_name": *"v[^"]*"' | head -1 | sed -E 's/.*"v([^"]+)".*/\1/')"; \ + fi; \ + echo "Installing PocketBase v${ver} (${TARGETARCH})"; \ + wget -q -O /tmp/pb.zip \ + "https://github.com/pocketbase/pocketbase/releases/download/v${ver}/pocketbase_${ver}_linux_${TARGETARCH}.zip"; \ + unzip /tmp/pb.zip -d /pb; \ + rm /tmp/pb.zip + +# API Server binary, built Web App static assets, and the weed binary. +COPY --from=api-build /out/api-server /usr/local/bin/api-server +COPY --from=web-build /app/dist /usr/share/nginx/html +COPY --from=seaweed /usr/bin/weed /usr/local/bin/weed + +# WebSocket handshakes need Connection/Upgrade forwarded, and the map deriving +# them must sit in the http context, not inside a server block. It goes in +# http.d/ (which Alpine nginx includes from http{}; it does not read conf.d/), +# and the 00- prefix keeps it ahead of default.conf in the include order. +RUN cat > /etc/nginx/http.d/00-upgrade.conf <<'NGINXMAP' +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} +NGINXMAP + +# nginx: serve the SPA and proxy /api/ + /ocpp/ to the API Server on localhost. +RUN cat > /etc/nginx/http.d/default.conf <<'NGINX' +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + gzip_min_length 1024; + + # A real liveness route, so the SPA fallback below cannot answer a health + # probe with index.html and make a broken container look healthy. + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 "ok"; + } + + location /api/ { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + # Forward WebSocket upgrades instead of silently stripping them. + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # The chargers' door, not the browser's. The API Server tells a charger to + # dial the host it was itself asked on — this one — so without this location + # the SPA fallback would answer the WebSocket handshake with index.html. + location /ocpp/ { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # A charging session is idle between heartbeats; the default 60s would + # close it under the charger. + proxy_read_timeout 1h; + proxy_send_timeout 1h; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} +NGINX + +# Bucket and identity seed, run by the S3 gateway's program before it serves. +# Two jobs, both idempotent, so every boot re-applies the values from the +# environment and changes nothing else — which is also how a rotated +# PB_S3_SECRET reaches the gateway: +# +# 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 the gateway below runs with neither an -config file nor +# AWS_ACCESS_KEY_ID: the env vars are the lowest-priority credential source in +# SeaweedFS, 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 +# PocketBase's key shows under Object Store → Users like any other. +# +# 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 +# never starts (supervisord retries it, and the healthcheck stays red). +RUN cat > /usr/local/bin/seaweedfs-seed <<'SEED' +#!/bin/sh +set -e +: "${PB_S3_ACCESS_KEY:?seaweedfs-seed: PB_S3_ACCESS_KEY is required}" +: "${PB_S3_SECRET:?seaweedfs-seed: PB_S3_SECRET is required}" +bucket="${PB_S3_BUCKET:-drivervault}" +shell() { weed shell -master=127.0.0.1:9333 -filer=127.0.0.1:8888; } +printf '%s\n' \ + "s3.bucket.create -name $bucket" \ + "s3.configure -user drivervault -access_key $PB_S3_ACCESS_KEY -secret_key $PB_S3_SECRET -actions Admin -apply" \ + | shell +if ! echo "s3.configure" | shell | grep -q "$PB_S3_ACCESS_KEY"; then + echo "seaweedfs-seed: PocketBase's identity is not in the filer's IAM store; refusing to start the gateway" >&2 + exit 1 +fi +SEED +RUN chmod +x /usr/local/bin/seaweedfs-seed + +# supervisord runs the eight processes and keeps them alive. Priorities only +# order the launches; readiness is the `until wget` loop in front of each +# program that needs another one up, the same chain the compose split spells +# out with depends_on + healthchecks: master → volume → filer → seed + S3 → +# PocketBase → API Server. +# +# Every SeaweedFS role advertises and binds 127.0.0.1 (-ip / -ip.bind): they only +# ever talk to each other in here, and the volume server in particular serves +# file content by id with no authentication at all. The S3 gateway and the admin +# UI bind everywhere so they can be published — on loopback, by the compose +# file's default. +RUN cat > /etc/supervisord.conf <<'SUPERVISOR' +[supervisord] +nodaemon=true +user=root +pidfile=/run/supervisord.pid +logfile=/dev/null +logfile_maxbytes=0 + +; SeaweedFS master: volume/topology metadata and file ids. +[program:seaweedfs-master] +user=app +command=/usr/local/bin/weed master -ip=127.0.0.1 -ip.bind=127.0.0.1 -port=9333 -mdir=/seaweed/data -volumeSizeLimitMB=1024 -metricsPort=9324 +priority=1 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; SeaweedFS volume server: where the bytes land. On 8081 because 8080 is the +; API Server in this container. -max=0 sizes itself from free disk rather than +; the default cap of 8 volumes. +[program:seaweedfs-volume] +user=app +command=/bin/sh -c 'until wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null 2>&1; do echo "waiting for seaweedfs master..."; sleep 1; done; exec /usr/local/bin/weed volume -master=127.0.0.1:9333 -ip=127.0.0.1 -ip.bind=127.0.0.1 -port=8081 -dir=/seaweed/data -max=0 -metricsPort=9325' +priority=2 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; SeaweedFS filer: the directory tree over the flat volume store — buckets, +; object keys — and the IAM store the S3 identities live in. -defaultStoreDir +; keeps its embedded leveldb on the data volume so they survive a recreate. +[program:seaweedfs-filer] +user=app +command=/bin/sh -c 'until wget -qO- http://127.0.0.1:8081/healthz >/dev/null 2>&1; do echo "waiting for seaweedfs volume..."; sleep 1; done; exec /usr/local/bin/weed filer -master=127.0.0.1:9333 -ip=127.0.0.1 -ip.bind=127.0.0.1 -port=8888 -defaultStoreDir=/seaweed/data -metricsPort=9326' +priority=3 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; SeaweedFS S3 gateway: PocketBase's endpoint. Seeds the bucket and identity +; first (see /usr/local/bin/seaweedfs-seed) and never serves if that fails. 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. +[program:seaweedfs-s3] +user=app +command=/bin/sh -c 'until wget -qO- http://127.0.0.1:8888/healthz >/dev/null 2>&1; do echo "waiting for seaweedfs filer..."; sleep 1; done; /usr/local/bin/seaweedfs-seed && exec /usr/local/bin/weed s3 -filer=127.0.0.1:8888 -ip.bind=0.0.0.0 -port=8333 -metricsPort=9327' +priority=4 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; SeaweedFS admin UI: cluster view, buckets, maintenance, and Object Store → +; Users. It can mint credentials for the bucket, and weed leaves auth off +; entirely when WEED_ADMIN_PASSWORD is empty — so an empty password means no +; panel at all rather than an open one. -dataDir persists the session key and +; the maintenance-task settings. +[program:seaweedfs-admin] +user=app +command=/bin/sh -c 'if [ -z "$WEED_ADMIN_PASSWORD" ]; then echo "seaweedfs-admin: WEED_ADMIN_PASSWORD is empty; refusing to serve an unauthenticated panel" >&2; exit 1; fi; until wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null 2>&1; do echo "waiting for seaweedfs master..."; sleep 1; done; exec /usr/local/bin/weed admin -port=23646 -master=127.0.0.1:9333 -dataDir=/seaweed/admin -metricsPort=9328' +priority=5 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; PocketBase: wait for the S3 gateway — it is the process that reads and writes +; the objects, so the bucket has to be serving before it does, exactly as the +; compose split gates the whole container on the gateway's health. Then upsert +; the superuser (idempotent) and serve. Runs as the unprivileged app user, which +; owns /pb and the pb_data volume. +[program:pocketbase] +directory=/pb +user=app +command=/bin/sh -c 'until wget -qO- http://127.0.0.1:8333/healthz >/dev/null 2>&1; do echo "waiting for seaweedfs s3..."; sleep 1; done; /pb/pocketbase superuser upsert "$PB_ADMIN_EMAIL" "$PB_ADMIN_PASSWORD" 2>/dev/null || true; exec /pb/pocketbase serve --http=0.0.0.0:8070' +priority=10 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; API Server: wait for PocketBase to be healthy, then start. Its bootstrap +; points PocketBase's file storage at the bucket and asks it to prove the +; gateway is reachable. It keeps no state on disk — plugin settings, like +; everything else it owns, live in PocketBase — so its working directory is +; just a place to run from. +[program:api-server] +directory=/app +user=app +command=/bin/sh -c 'until wget -qO- http://127.0.0.1:8070/api/health >/dev/null 2>&1; do echo "waiting for pocketbase..."; sleep 1; done; exec /usr/local/bin/api-server' +priority=20 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; nginx stays root so it can bind :80; its own workers drop to the nginx user. +[program:nginx] +command=/usr/sbin/nginx -g 'daemon off;' +priority=30 +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +SUPERVISOR + +# The entrypoint stays root only long enough to make the data volumes writable +# by the app user, then hands off to supervisord. The chown matters for a host +# bind mount, which arrives owned by root rather than inheriting the image's +# owner. +RUN cat > /entrypoint.sh <<'ENTRY' +#!/bin/sh +set -e +for dir in /pb/pb_data /seaweed/data /seaweed/admin; do + mkdir -p "$dir" + if [ "$(stat -c %U "$dir" 2>/dev/null)" != "app" ]; then + echo "entrypoint: taking ownership of $dir" + chown -R app:app "$dir" + fi +done +exec supervisord -c /etc/supervisord.conf +ENTRY +RUN chmod +x /entrypoint.sh + +# API Server config: everything is local to this container. WEBAPP_URL is what +# the panel status page probes; nginx serves the Web App on :80 in here, so the +# stock default of localhost:8090 would always report the Web App as down. +# +# File storage is on by construction: the gateway is in this image, at a fixed +# loopback address, path style because a self-hosted gateway has no per-bucket +# DNS. The region is a formality SeaweedFS ignores and PocketBase insists on. +# supervisord passes these through to the API Server, whose bootstrap writes +# them into PocketBase's settings on every boot. Only record files move — scans, +# receipts, invoices, part photos; the database and PocketBase's own backups +# stay on /pb/pb_data. +ENV API_ADDR=:8080 \ + POCKETBASE_URL=http://127.0.0.1:8070 \ + CORS_ALLOW_ORIGINS=http://localhost:8090 \ + AUTH_USERS_COLLECTION=users \ + WEBAPP_URL=http://127.0.0.1:80 \ + PB_S3_ENABLED=true \ + PB_S3_ENDPOINT=http://127.0.0.1:8333 \ + PB_S3_BUCKET=drivervault \ + PB_S3_REGION=us-east-1 \ + PB_S3_FORCE_PATH_STYLE=true \ + WEED_ADMIN_USER=admin + +# Required at runtime (no safe defaults): PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD; +# PB_S3_ACCESS_KEY, PB_S3_SECRET (the identity seeded into SeaweedFS AND what +# PocketBase authenticates with — the gateway refuses to start without them); +# WEED_ADMIN_PASSWORD (the admin UI refuses to start without it). +# Optional: DRIVERVAULT_SUPERADMIN_EMAIL / DRIVERVAULT_SUPERADMIN_PASSWORD create +# the first app super-admin on boot; WEED_ADMIN_READONLY_USER / _PASSWORD add a +# view-only login to the admin UI. PB_BOOTSTRAP=false skips schema setup — +# leave it on: a release can add collections or fields the server needs, and a +# stack that skips the bootstrap never gets them. (app_settings, which holds the +# plugin settings, is created on demand; nothing else is.) +# For Anker Solix charger control, OCPP_REQUIRE_TLS (default true) rejects +# chargers that did not arrive over TLS — this image serves plain HTTP, so put a +# TLS-terminating proxy in front and set OCPP_PUBLIC_URL to the public wss:// +# base, or set OCPP_REQUIRE_TLS=false on a trusted network. +# Pass them with `docker run -e ...`. + +# Three volumes. /pb/pb_data: the database, the uploads made before S3 was on, +# PocketBase's own backups, and the server settings — the API Server keeps no +# state on disk. /seaweed/data: master, volume and filer share it, in exactly +# the layout `weed server -dir` writes (master raft state, volume .dat/.idx, the +# filer's filerldb2/ — no filename overlap), so a SEAWEED_DATA volume from +# either compose SeaweedFS file mounts here unchanged, and vice versa. +# /seaweed/admin: the admin UI's session key and maintenance-task state, small +# and no part of the object store. All pre-created and owned by app so a fresh +# named volume inherits that ownership. +RUN mkdir -p /pb/pb_data /seaweed/data /seaweed/admin /app \ + && chown -R app:app /pb /seaweed /app +VOLUME ["/pb/pb_data", "/seaweed/data", "/seaweed/admin"] +# 80 = Web App, 8070 = PocketBase admin, 8080 = API Server + embedded API panel +# (also the /ocpp/{serial} endpoint chargers dial into), 8333 = S3 gateway (for +# aws-cli and the like; loopback is enough), 23646 = SeaweedFS admin UI. +EXPOSE 80 8070 8080 8333 23646 + +# Every process must answer, so a wedged component shows up in `docker ps` +# instead of a container that looks up while part of it is dead. start-period +# covers the SeaweedFS chain plus the first-boot schema bootstrap on a cold +# database. +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:8081/healthz >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:8888/healthz >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:8333/healthz >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:23646/health >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:8070/api/health >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1 \ + && wget -qO- http://127.0.0.1:80/healthz >/dev/null 2>&1 \ + || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Docker-AIO/README.md b/Docker-AIO/README.md index 10145ca..1ea8026 100644 --- a/Docker-AIO/README.md +++ b/Docker-AIO/README.md @@ -15,6 +15,7 @@ scale, upgrade or restart the pieces independently. | File | Use | |---|---| | `Dockerfile` | the all-in-one image (build context must be the **repo root**) | +| `Dockerfile.seaweedfs.split` | the same, with SeaweedFS split into its roles baked in — see below | | `docker-compose.yml` | **builds from source** — for development and local testing | | `docker-compose.prod.yml` | **pulls the prebuilt image** from the registry | | `.env.example` / `.env.prod.example` | copy to `.env` for the matching compose file | @@ -105,7 +106,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` | +| **SeaweedFS, split into its roles** | `docker-compose.prod.seaweedfs.split.yml` (builds `Dockerfile.seaweedfs.split`) | `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 @@ -131,12 +132,27 @@ 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 +one process. `docker-compose.seaweedfs.split.yml` runs 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. +later. + +The prod twin goes the other way: `docker-compose.prod.seaweedfs.split.yml` +**builds `Dockerfile.seaweedfs.split`**, which bakes all five roles into the +all-in-one image as five more supervisord programs (the `weed` binary is copied +from the official image, pinned by the `SEAWEED_VERSION` build arg). One +container, three volumes (`PB_DATA`, `SEAWEED_DATA`, `SEAWEED_ADMIN_DATA`), and +the bucket-and-identity seed runs inside the gateway's program instead of a +`seaweedfs-init` container. What survives of "split" in there is per-role +restart, per-role metrics ports and the admin UI; what does not is upgrading +SeaweedFS by changing a tag — it is a rebuild — and spreading the roles over +hosts. Inside, every role listens on loopback (the volume server on **8081**, +since 8080 is the API Server), and only the gateway and the admin UI are +published, on loopback by default. The build needs the repo checkout, so either +build on the deploy host or `docker compose push` the image to `AIO_IMAGE` and +`pull` it there. Identities work differently there, and it matters. SeaweedFS reads credentials from, in descending priority: an `-s3.config` file, the filer's IAM store, then @@ -152,16 +168,17 @@ 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. +the bucket (the baked-in image refuses to start the panel at all without one). +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. Neither file publishes +anything 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. +Switching between any of the SeaweedFS files needs no migration: master, volume +and filer share one data mount (`/data` in the side containers, `/seaweed/data` +in the baked-in image), which is exactly the layout `weed server -dir` 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 diff --git a/Docker-AIO/docker-compose.prod.seaweedfs.split.yml b/Docker-AIO/docker-compose.prod.seaweedfs.split.yml index f526c90..ad3df60 100644 --- a/Docker-AIO/docker-compose.prod.seaweedfs.split.yml +++ b/Docker-AIO/docker-compose.prod.seaweedfs.split.yml @@ -1,261 +1,73 @@ 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. +# Production all-in-one with SeaweedFS split into its roles — and, unlike the +# other prod files, built into ONE image here: Dockerfile.seaweedfs.split bakes +# master, volume, filer, S3 gateway and the admin UI into the same container as +# PocketBase, the API Server and the Web App. One service, one container, three +# volumes. 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 +# 2. docker compose -f docker-compose.prod.seaweedfs.split.yml build # 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 build context is the repo root, so this has to run on a host that has the +# checkout. To deploy elsewhere, `push` the built image to the registry named in +# AIO_IMAGE and `pull` it there — `image:` and `build:` are both set, so the +# same file does either. # -# • 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 the split buys inside one image: per-role restart under supervisord, +# per-role metrics ports, and the SeaweedFS admin UI (weed admin) — a cluster +# view, and Object Store → Users, where S3 identities are created and revoked +# without touching a file. What it costs against the six-container shape it +# replaces: a new SeaweedFS is a rebuild (SEAWEED_VERSION below), not a tag +# change, and the roles cannot be spread over hosts. If the object store should +# stay outside the app image, use docker-compose.prod.seaweedfs.yml — the S3 +# behaviour is identical. # -# 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. +# The on-disk layout is the same as every other SeaweedFS shape here: 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 a SEAWEED_DATA volume from docker-compose.prod.seaweedfs.yml +# mounts here unchanged, and vice versa, with no migration either way. # -# 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. +# Inside the container every SeaweedFS role listens on loopback except the S3 +# gateway and the admin UI, and those two are published on loopback by default, +# the way the other SeaweedFS files publish them. The volume server serves file +# content by id with no authentication at all, so it is never reachable from +# outside; the admin UI shows what its port would. # # 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. +# On first boot the S3 gateway's program creates the bucket and seeds +# PocketBase's identity into the filer's IAM store, PocketBase upserts the +# superuser from PB_ADMIN_*, and the API Server creates any missing collections, +# the DriverVault super-admin from DRIVERVAULT_SUPERADMIN_*, and points +# PocketBase's file storage at the bucket. Every step is idempotent. services: - # --- 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}" + build: + # Project root (one level up from this compose file), so the Dockerfile + # can reach both "API Server/" and "Web App/". + context: .. + dockerfile: Docker-AIO/Dockerfile.seaweedfs.split + args: + # Empty -> bundle uses same-origin "/api", proxied internally by nginx. + - VITE_API_BASE=${VITE_API_BASE:-} + # Bare names = pass through only when set in the environment, so an + # unset PB_VERSION / SEAWEED_VERSION leaves the Dockerfile pin in place + # instead of overriding it with an empty string (which for PB_VERSION + # would resolve "latest" at build time, and for SEAWEED_VERSION would + # not build at all). + - PB_VERSION + - SEAWEED_VERSION + # Tagged for the registry so `docker compose push` lands it where `pull` + # on the deploy host expects it. + image: "${AIO_IMAGE:-10.2.1.10:5500/admin/drivervault-aio-seaweedfs-split: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}" @@ -285,43 +97,71 @@ services: 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" + # The image fixes the rest (PB_S3_ENABLED, the loopback endpoint, path + # style, the region): the gateway is inside, at an address that cannot + # change. supervisord passes these through to the API Server, whose + # bootstrap writes them into PocketBase's settings on every boot, + # idempotently. Only record files move — scans, receipts, invoices, part + # photos. The database and PocketBase's own backups stay on PB_DATA. + # + # The credentials do double duty: the gateway's program seeds them into + # the filer's IAM store as the identity named "drivervault" (updating it + # in place on every boot — that is how a rotated secret lands) *and* they + # are what PocketBase authenticates with. No safe defaults; the gateway + # refuses to start without them, and with it the whole container. PB_S3_BUCKET: "${PB_S3_BUCKET:-drivervault}" - # 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" + 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}" + # --- SeaweedFS admin UI -------------------------------------------- + # 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, and the image refuses to + # start the panel at all in that case. Read from WEED_ADMIN_* rather + # than a flag, which keeps it off the process command line. + 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:-}" ports: - "${WEB_PORT:-8090}:80" # Web App - "${PB_PORT:-8070}:8070" # PocketBase admin UI / API - "${API_PORT:-8080}:8080" # API Server + panel (root /) + /ocpp/{serial} + # The S3 gateway, loopback only: PocketBase reaches it inside the + # container, so this is here for `aws s3 ls --endpoint-url + # http://127.0.0.1:8333` and nothing else. Set SEAWEED_S3_BIND=0.0.0.0 to + # expose it, and mean it. + - "${SEAWEED_S3_BIND:-127.0.0.1}:${SEAWEED_S3_PORT:-8333}:8333" + # The admin UI, loopback for the same reason: 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. + - "${SEAWEED_ADMIN_BIND:-127.0.0.1}:${SEAWEED_ADMIN_PORT:-23646}:23646" 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. + # Named volumes by default; set any of them to a host path in .env for a + # bind mount. The API Server keeps no state on disk, so everything it + # owns (plugin settings included) is in PB_DATA. Back up PB_DATA and + # SEAWEED_DATA together — from here on the attachments live in the + # second, not the first. - "${PB_DATA:-pb_data}:/pb/pb_data" + - "${SEAWEED_DATA:-seaweed_data}:/seaweed/data" + # The admin UI's own small volume: session key and maintenance state, no + # object data. + - "${SEAWEED_ADMIN_DATA:-seaweed_admin}:/seaweed/admin" 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"] + # Every process must answer — the five SeaweedFS roles and the three app + # processes. Declared here as well as in the image so the check is + # visible, and works against an older pulled image. start_period covers + # the SeaweedFS chain plus the first-boot schema bootstrap. + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null && wget -qO- http://127.0.0.1:8081/healthz >/dev/null && wget -qO- http://127.0.0.1:8888/healthz >/dev/null && wget -qO- http://127.0.0.1:8333/healthz >/dev/null && wget -qO- http://127.0.0.1:23646/health >/dev/null && wget -qO- http://127.0.0.1:8070/api/health >/dev/null && wget -qO- http://127.0.0.1:8080/healthz >/dev/null && wget -qO- http://127.0.0.1:80/healthz >/dev/null || exit 1"] interval: 30s - timeout: 5s + timeout: 10s retries: 3 - start_period: 60s + start_period: 90s volumes: pb_data: - # Shared by master, volume and filer — the same layout `weed server -dir` + # Master, volume and filer share it — the same layout `weed server -dir` # writes, so this file and docker-compose.prod.seaweedfs.yml can swap places # on it. seaweed_data: