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>
362 lines
16 KiB
Docker
362 lines
16 KiB
Docker
# 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"]
|