diff --git a/Docker-AIO/.env.prod.seaweedfs.example b/Docker-AIO/.env.prod.seaweedfs.example index d4f046a..3b58b57 100644 --- a/Docker-AIO/.env.prod.seaweedfs.example +++ b/Docker-AIO/.env.prod.seaweedfs.example @@ -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 diff --git a/Docker-AIO/Dockerfile.seaweedfs b/Docker-AIO/Dockerfile.seaweedfs new file mode 100644 index 0000000..04764a3 --- /dev/null +++ b/Docker-AIO/Dockerfile.seaweedfs @@ -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"] diff --git a/Docker-AIO/README.md b/Docker-AIO/README.md index 1ea8026..1a58546 100644 --- a/Docker-AIO/README.md +++ b/Docker-AIO/README.md @@ -15,6 +15,7 @@ scale, upgrade or restart the pieces independently. | File | Use | |---|---| | `Dockerfile` | the all-in-one image (build context must be the **repo root**) | +| `Dockerfile.seaweedfs` | 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 diff --git a/Docker-AIO/docker-compose.prod.seaweedfs.yml b/Docker-AIO/docker-compose.prod.seaweedfs.yml index 1b5e1db..f795068 100644 --- a/Docker-AIO/docker-compose.prod.seaweedfs.yml +++ b/Docker-AIO/docker-compose.prod.seaweedfs.yml @@ -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: