From 28f5fda45102103f54a7cd0b4b064517557d280b Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:43:49 +0200 Subject: [PATCH] The split object store moves into the image it was kept out of Dockerfile.seaweedfs.split bakes SeaweedFS master, volume, filer, S3 gateway and admin UI into the all-in-one as five more supervisord programs, with the weed binary copied from the official image and pinned by SEAWEED_VERSION. The readiness chain the compose split expressed with depends_on and healthchecks becomes until-wget loops: master, volume, filer, then the gateway, which seeds the bucket and PocketBase's identity and refuses to serve if the grep gate fails. The volume server sits on 8081 because 8080 is the API Server; every role except the gateway and the admin UI binds loopback. docker-compose.prod.seaweedfs.split.yml goes from six services to one, with build: and image: both set so the same file builds, pushes and pulls, under a registry name of its own since this image expects three volumes. The env example and README follow. Co-Authored-By: Claude Opus 5 --- Docker-AIO/.env.prod.seaweedfs.split.example | 57 ++- Docker-AIO/Dockerfile.seaweedfs.split | 462 ++++++++++++++++++ Docker-AIO/README.md | 47 +- .../docker-compose.prod.seaweedfs.split.yml | 368 ++++---------- 4 files changed, 630 insertions(+), 304 deletions(-) create mode 100644 Docker-AIO/Dockerfile.seaweedfs.split 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: