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:
1 parent
28f5fda451
commit
c9a95131a5
4 files changed
+486
-129
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
Reference in new issue
Block a user