The single-process object store follows its split sibling into the image

Dockerfile.seaweedfs bakes `weed server -s3` into the all-in-one as a
fourth supervisord program, sharing every build stage and the nginx
config with Dockerfile.seaweedfs.split. The gateway's single identity is
mapped from the PB_S3_* pair PocketBase already authenticates with, and
an empty pair refuses to start rather than serving allow-anyone. The
seaweedfs-init container becomes a step in PocketBase's program — wait
for the gateway, create the bucket, then serve — so the bucket exists
before the API Server's bootstrap checks it, in the order compose kept
with depends_on. The volume server moves to 8081 because 8080 is the API
Server; weed server has one bind flag for all four roles, so it stays
open and the comment says never to publish it.

docker-compose.prod.seaweedfs.yml goes from three services to one, with
build: and image: both set under a registry name of its own. The env
example and README follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5 committed 2026-09-11 14:59:16 +02:00
1 parent 28f5fda451
commit c9a95131a5
4 files changed
+486 -129

No files matched your search

+23 -16
View File
@@ -1,10 +1,17 @@
# DriverVault all-in-one — production config.
# Copy to .env and fill in, then:
# docker compose -f docker-compose.prod.seaweedfs.yml pull
# DriverVault all-in-one — production config, SeaweedFS built into the same
# image (Dockerfile.seaweedfs). Copy to .env and fill in, then:
# docker compose -f docker-compose.prod.seaweedfs.yml build
# docker compose -f docker-compose.prod.seaweedfs.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 two volumes.
AIO_IMAGE=10.2.1.10:5500/admin/drivervault-aio-seaweedfs: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.
@@ -57,9 +64,9 @@ API_PORT=8080
# --- Storage -----------------------------------------------------------------
# One Docker-managed named volume by default. Set it to an absolute host path
# for a bind mount, e.g. PB_DATA=/srv/drivervault/pb_data.
# PB_DATA — the PocketBase database and uploads. It is the only volume in the
# image: the API Server keeps no state on disk, so everything it owns (plugin
# settings included) is backed up by backing up this one path.
# PB_DATA — the PocketBase database and its backups. The API Server keeps no
# state on disk, so everything it owns (plugin settings included) is backed up
# by backing up this one path — together with SEAWEED_DATA below.
PB_DATA=pb_data
# --- File storage: SeaweedFS -------------------------------------------------
@@ -69,23 +76,23 @@ PB_DATA=pb_data
#
# The credentials do double duty: they configure the SeaweedFS gateway's single
# identity *and* are what PocketBase authenticates with. There are no safe
# defaults, and the stack refuses to start without them.
# defaults, and the container refuses to start without them.
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 PocketBase's program, before it serves.
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.
# It is mounted at /seaweed/data, the layout `weed server -dir` writes — the
# same one the .split. files use, so the two are interchangeable on it.
SEAWEED_DATA=seaweed_data
# The gateway image, pinned so a redeploy months from now brings up the same one.
# 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
+361
View File
@@ -0,0 +1,361 @@
# syntax=docker/dockerfile:1
#
# All-in-one image WITH the object store inside: PocketBase + API Server + Web
# App, plus SeaweedFS as one `weed server -s3` process — master, volume, filer
# and S3 gateway as goroutines — under supervisord in the same container.
#
# This is Dockerfile with the two SeaweedFS containers of
# docker-compose.prod.seaweedfs.yml folded in. That compose file 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 it costs: a
# new SeaweedFS means a rebuild (--build-arg SEAWEED_VERSION). For per-role
# restart, metrics and the admin UI, see Dockerfile.seaweedfs.split.
#
# 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" -t drivervault-aio-seaweedfs .
#
# Run it (everything starts together):
#
# docker run -d --name drivervault -p 80:80 -p 8070:8070 -p 8080:8080 \
# -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 \
# -v drivervault_pb:/pb/pb_data \
# -v drivervault_seaweed:/seaweed/data \
# drivervault-aio-seaweedfs
#
# Then: web app on http://host/ and PocketBase admin on http://host:8070/_/.
# On first boot PocketBase's program creates the bucket, 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 and 8333 are meant to be
# published):
#
# 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
# 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 SeaweedFS. 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
# supervisord runs the four 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 file spells out
# with depends_on + healthchecks: SeaweedFS → bucket → PocketBase → API Server.
RUN cat > /etc/supervisord.conf <<'SUPERVISOR'
[supervisord]
nodaemon=true
user=root
pidfile=/run/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
; SeaweedFS: one process, four roles — master, volume, filer and the S3
; gateway. -dir is the only state it keeps. -ip=127.0.0.1 is the address the
; roles advertise to each other, which is where they are; -ip.bind stays open
; so the gateway can be published (weed server has one bind flag for all four,
; and the volume server, on 8081 because 8080 is the API Server, serves file
; content by id with no authentication — so never publish 8081).
;
; SeaweedFS falls back to AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY when started
; without an -s3.config file, and configuring one identity is what takes the
; gateway out of its default allow-anyone mode. They are the PB_S3_* pair
; PocketBase authenticates with, mapped here so there is one pair to set — and
; an empty pair means no gateway rather than an open one.
[program:seaweedfs]
user=app
command=/bin/sh -c 'if [ -z "$PB_S3_ACCESS_KEY" ] || [ -z "$PB_S3_SECRET" ]; then echo "seaweedfs: PB_S3_ACCESS_KEY and PB_S3_SECRET are required; refusing to start an unauthenticated gateway" >&2; exit 1; fi; export AWS_ACCESS_KEY_ID="$PB_S3_ACCESS_KEY" AWS_SECRET_ACCESS_KEY="$PB_S3_SECRET"; exec /usr/local/bin/weed server -dir=/seaweed/data -ip=127.0.0.1 -ip.bind=0.0.0.0 -volume.port=8081 -s3 -master.volumeSizeLimitMB=1024'
priority=1
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 file gates the whole container on the gateway's health. Then create
; the bucket: PocketBase never issues a CreateBucket of its own and SeaweedFS
; will not conjure one on first upload. Creating a bucket that already exists is
; a no-op, and `|| true` keeps a restart from being blocked by the shell's exit
; status — a gateway that is genuinely broken is reported by the API Server's
; own S3 check at boot, with the reason. 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..."; sleep 1; done; echo "s3.bucket.create -name $PB_S3_BUCKET" | /usr/local/bin/weed shell -master=127.0.0.1:9333 || true; /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; 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
# Required at runtime (no safe defaults): PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD;
# PB_S3_ACCESS_KEY, PB_S3_SECRET (the gateway's single identity AND what
# PocketBase authenticates with — the gateway refuses to start without them).
# Optional: DRIVERVAULT_SUPERADMIN_EMAIL / DRIVERVAULT_SUPERADMIN_PASSWORD create
# the first app super-admin on boot. 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 ...`.
# Two 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: everything SeaweedFS keeps, in the layout
# `weed server -dir` writes — the same one the split image's master, volume and
# filer share, so a SEAWEED_DATA volume from any of the SeaweedFS compose files
# mounts here unchanged, and vice versa. Both pre-created and owned by app so a
# fresh named volume inherits that ownership.
RUN mkdir -p /pb/pb_data /seaweed/data /app \
&& chown -R app:app /pb /seaweed /app
VOLUME ["/pb/pb_data", "/seaweed/data"]
# 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).
EXPOSE 80 8070 8080 8333
# 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 SeaweedFS coming up 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:8333/healthz >/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"]
+27 -23
View File
@@ -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` | the same, with SeaweedFS (`weed server -s3`) baked in — see below |
| `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 |
@@ -105,7 +106,7 @@ remember — with an `.env` example of the same name:
| Shape | From the registry | From source |
|---|---|---|
| **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 beside the image** | `docker-compose.prod.seaweedfs.yml` (builds `Dockerfile.seaweedfs`) | `docker-compose.seaweedfs.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` |
@@ -120,14 +121,21 @@ docker compose -f docker-compose.prod.seaweedfs.yml up -d
```
Set `PB_S3_ACCESS_KEY` and `PB_S3_SECRET` first — both storage files refuse to
start without them. The SeaweedFS ones run the gateway as a **second container**
(master, volume, filer and S3 in one process, on its own `seaweed_data` volume)
rather than a fourth process under supervisord: keeping the object store in this
image, on the volume the files are being moved off, would defeat the point and
would mean rebuilding. They also run a one-shot `seaweedfs-init` that creates the
bucket, because PocketBase never issues a `CreateBucket` of its own. The
external-S3 ones add no containers at all: set `PB_S3_ENDPOINT`, and create the
bucket yourself.
start without them. The from-source SeaweedFS files run the gateway as a
**second container** (master, volume, filer and S3 in one process, on its own
`seaweed_data` volume) plus a one-shot `seaweedfs-init` that creates the bucket,
because PocketBase never issues a `CreateBucket` of its own. The prod SeaweedFS
files go the other way and **build it into the image**: `Dockerfile.seaweedfs`
runs `weed server -s3` as a fourth supervisord program (the `weed` binary is
copied from the official image, pinned by the `SEAWEED_VERSION` build arg), on
a `/seaweed/data` volume of its own, with PocketBase's program creating the
bucket before it serves. One container, two volumes; the cost is that a new
SeaweedFS is a rebuild rather than a tag change. Inside, the volume server sits
on **8081** since 8080 is the API Server, and only the gateway is 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. The external-S3 ones add no containers at all: set `PB_S3_ENDPOINT`, and
create the bucket yourself.
### Split SeaweedFS
@@ -139,20 +147,16 @@ 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.
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.
The prod twin, like its non-split sibling, builds it in:
`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. 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 spreading the roles over hosts. Inside, every role listens on
loopback, and only the gateway and the admin UI are published, on loopback by
default.
Identities work differently there, and it matters. SeaweedFS reads credentials
from, in descending priority: an `-s3.config` file, the filer's IAM store, then
+75 -90
View File
@@ -1,13 +1,21 @@
name: drivervault-aio
# Production all-in-one, with SeaweedFS — pulls the prebuilt image from the
# registry instead of building. Self-contained: one file, no overlays.
# Everything an operator needs to set lives in .env.
# Production all-in-one with SeaweedFS — and, unlike docker-compose.prod.yml,
# built into ONE image here: Dockerfile.seaweedfs bakes `weed server -s3`
# (master, volume, filer and S3 gateway in one process) into the same container
# as PocketBase, the API Server and the Web App. One service, one container,
# two volumes. Self-contained: one file, no overlays. Everything an operator
# needs to set lives in .env.
#
# 1. cp .env.prod.seaweedfs.example .env (then edit it)
# 2. docker compose -f docker-compose.prod.seaweedfs.yml pull
# 2. docker compose -f docker-compose.prod.seaweedfs.yml build
# 3. docker compose -f docker-compose.prod.seaweedfs.yml up -d
#
# The build context is the repo root, so this has to run on a host that has the
# checkout. To deploy elsewhere, `push` the built image to the registry named in
# AIO_IMAGE and `pull` it there — `image:` and `build:` are both set, so the
# same file does either.
#
# This is docker-compose.prod.yml plus an S3 object store: PocketBase keeps its
# record files — document scans, service and refill receipts, workshop invoices,
# part photos — in a SeaweedFS bucket instead of on the pb_data volume next to
@@ -15,79 +23,42 @@ name: drivervault-aio
# Clients cannot tell the difference: an attachment has always been fetched
# through the API Server, never from a storage URL.
#
# SeaweedFS runs as a second container beside the all-in-one, not as a fourth
# process inside it: keeping the object store in that image, on the volume the
# files are being moved off, would defeat the point and would mean rebuilding.
# What baking it in costs against the two-container shape it replaces: a new
# SeaweedFS is a rebuild (SEAWEED_VERSION below), not a tag change. For
# per-role restart, metrics and the SeaweedFS admin UI, see
# docker-compose.prod.seaweedfs.split.yml — the S3 behaviour is identical, and
# the data volume is interchangeable (both write the `weed server -dir` layout).
#
# Before turning this on for a stack that already has uploads: PocketBase does
# NOT copy existing files into the bucket. See README.md.
#
# On first boot PocketBase 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 PocketBase's program creates the bucket and upserts the
# superuser from PB_ADMIN_*, and the API Server creates any missing
# collections, the DriverVault super-admin from DRIVERVAULT_SUPERADMIN_*, and
# points PocketBase's file storage at the bucket. Every step is idempotent.
services:
seaweedfs:
image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}"
container_name: drivervault-aio-seaweedfs
restart: unless-stopped
# One process, four roles: master, volume, filer and the S3 gateway. -dir is
# the only state it keeps.
command: server -dir=/data -s3 -master.volumeSizeLimitMB=1024
environment:
# SeaweedFS falls back to these when started without an -s3.config file,
# and configuring one identity is what takes the S3 gateway out of its
# default allow-anyone mode. The same credentials PocketBase authenticates
# with below — one pair to set, in .env.
AWS_ACCESS_KEY_ID: "${PB_S3_ACCESS_KEY:?set PB_S3_ACCESS_KEY in .env}"
AWS_SECRET_ACCESS_KEY: "${PB_S3_SECRET:?set PB_S3_SECRET in .env}"
volumes:
# Named volume by default; set SEAWEED_DATA to a host path in .env for a
# bind mount, exactly as PB_DATA works. Back it up alongside PB_DATA —
# from here on the attachments live here, not in the database volume.
- "${SEAWEED_DATA:-seaweed_data}:/data"
ports:
# Loopback only: the stack reaches the gateway over the compose network,
# so this is here for `aws s3 ls --endpoint-url http://127.0.0.1:8333` and
# nothing else. Set SEAWEED_S3_BIND=0.0.0.0 to expose it, and mean it.
- "${SEAWEED_S3_BIND:-127.0.0.1}:${SEAWEED_S3_PORT:-8333}:8333"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status || exit 1"]
interval: 10s
timeout: 3s
retries: 12
start_period: 20s
seaweedfs-init:
image: "${SEAWEED_IMAGE:-chrislusf/seaweedfs:4.45}"
container_name: drivervault-aio-seaweedfs-init
# Runs once and exits. PocketBase never issues a CreateBucket of its own and
# SeaweedFS will not conjure one on first upload, so something has to.
# Creating a bucket that already exists is a no-op, so every later boot
# passes straight through.
restart: "no"
depends_on:
seaweedfs:
condition: service_healthy
entrypoint: ["/bin/sh", "-c"]
# `|| true` so a restart is never blocked by the shell's exit status: this
# step is best-effort, and a gateway that is genuinely unreachable is
# reported by the API Server's own S3 check at boot, with the reason.
command:
- 'echo "s3.bucket.create -name ${PB_S3_BUCKET:-drivervault}" | weed shell -master=seaweedfs:9333 || true'
drivervault:
image: "${AIO_IMAGE:-10.2.1.10:5500/admin/drivervault-aio:latest}"
build:
# Project root (one level up from this compose file), so the Dockerfile
# can reach both "API Server/" and "Web App/".
context: ..
dockerfile: Docker-AIO/Dockerfile.seaweedfs
args:
# Empty -> bundle uses same-origin "/api", proxied internally by nginx.
- VITE_API_BASE=${VITE_API_BASE:-}
# Bare names = pass through only when set in the environment, so an
# unset PB_VERSION / SEAWEED_VERSION leaves the Dockerfile pin in place
# instead of overriding it with an empty string (which for PB_VERSION
# would resolve "latest" at build time, and for SEAWEED_VERSION would
# not build at all).
- PB_VERSION
- SEAWEED_VERSION
# Tagged for the registry so `docker compose push` lands it where `pull`
# on the deploy host expects it.
image: "${AIO_IMAGE:-10.2.1.10:5500/admin/drivervault-aio-seaweedfs:latest}"
container_name: drivervault-aio
restart: unless-stopped
depends_on:
# PocketBase — inside this container — is the process that reads and
# writes the objects, so the gateway has to be serving first, and the
# bucket has to exist before the bootstrap points PocketBase at it.
seaweedfs:
condition: service_healthy
seaweedfs-init:
condition: service_completed_successfully
environment:
# Superuser (also used by the API Server to authenticate to PocketBase).
PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL:?set PB_ADMIN_EMAIL in .env}"
@@ -117,39 +88,53 @@ 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: they configure the gateway's single
# identity — which is what takes it out of its default allow-anyone mode
# — *and* they are what PocketBase authenticates with. No safe defaults;
# the gateway refuses to start without them, and with it the whole
# container.
PB_S3_BUCKET: "${PB_S3_BUCKET:-drivervault}"
# The service name: a server-to-server call inside the compose network.
PB_S3_ENDPOINT: "http://seaweedfs:8333"
# SeaweedFS ignores the region; PocketBase insists on having one.
PB_S3_REGION: "${PB_S3_REGION:-us-east-1}"
PB_S3_ACCESS_KEY: "${PB_S3_ACCESS_KEY}"
PB_S3_SECRET: "${PB_S3_SECRET}"
# Path style, because a self-hosted gateway has no per-bucket DNS.
PB_S3_FORCE_PATH_STYLE: "true"
PB_S3_ACCESS_KEY: "${PB_S3_ACCESS_KEY:?set PB_S3_ACCESS_KEY in .env}"
PB_S3_SECRET: "${PB_S3_SECRET:?set PB_S3_SECRET in .env}"
ports:
- "${WEB_PORT:-8090}:80" # Web App
- "${PB_PORT:-8070}:8070" # PocketBase admin UI / API
- "${API_PORT:-8080}:8080" # API Server + panel (root /) + /ocpp/{serial}
# The S3 gateway, loopback only: PocketBase reaches it inside the
# container, so this is here for `aws s3 ls --endpoint-url
# http://127.0.0.1:8333` and nothing else. Set SEAWEED_S3_BIND=0.0.0.0 to
# expose it, and mean it. Nothing else of SeaweedFS is published: the
# volume server (8081 inside) serves file content by id with no
# authentication at all.
- "${SEAWEED_S3_BIND:-127.0.0.1}:${SEAWEED_S3_PORT:-8333}:8333"
volumes:
# 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 either to a host path in .env for a bind
# mount. The API Server keeps no state on disk, so everything it owns
# (plugin settings included) is in PB_DATA. Back up PB_DATA and
# SEAWEED_DATA together — from here on the attachments live in the
# second, not the first.
- "${PB_DATA:-pb_data}:/pb/pb_data"
- "${SEAWEED_DATA:-seaweed_data}:/seaweed/data"
healthcheck:
# 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 — SeaweedFS's master and gateway and the three
# app processes. Declared here as well as in the image so the check is
# visible, and works against an older pulled image. start_period covers
# SeaweedFS coming up plus the first-boot schema bootstrap.
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9333/cluster/status >/dev/null && wget -qO- http://127.0.0.1:8333/healthz >/dev/null && wget -qO- http://127.0.0.1:8070/api/health >/dev/null && wget -qO- http://127.0.0.1:8080/healthz >/dev/null && wget -qO- http://127.0.0.1:80/healthz >/dev/null || exit 1"]
interval: 30s
timeout: 5s
timeout: 10s
retries: 3
start_period: 60s
start_period: 90s
volumes:
pb_data:
# The `weed server -dir` layout, so this file and its .split. twin can swap
# places on it.
seaweed_data: