Files
DriverVault/Docker-AIO/Dockerfile
T
tajniak81andClaude Opus 5 ee4ac441be Plugins: drop the plugins.json migration, and the volume it needed
The project has no public installs, so there is nothing to migrate from.
MigrateLegacyFile, the file-backed Store it read through, PLUGINS_FILE and
the legacy path threaded through the Server all go. What is left is one
store, PocketBase, and a plugins package that touches no filesystem at all.

That was the last thing keeping api_data alive, so the volume goes too. All
four compose files now declare exactly one volume, pb_data, and the
standalone API Server compose declares none - it talks to an external
PocketBase and has nothing of its own to keep. Backing up the stack is
backing up one path again.

Both images get simpler for it. The API Server image loses VOLUME /data and
the su-exec entrypoint that existed only to fix a mounted volume's
ownership, so it goes back to a plain USER app; its working directory is
now /app and holds nothing. The AIO image loses its second volume and
chowns only /pb/pb_data.

One consequence worth stating plainly, because it is a small regression
rather than a no-op. The panel's Settings -> PocketBase and Settings -> Web
App screens write .env in the working directory, which is now ephemeral. In
the multi-container stack that changes nothing: compose sets all five of
those keys as container environment, and loadDotEnv only applies a key that
is not already set, so the file could never win a restart there anyway. In
the AIO image it did win for POCKETBASE_ADMIN_EMAIL/_PASSWORD, which are
not in that container's environment - so a service account fixed from the
panel now lasts only until the container is recreated. Both READMEs say so.
Moving those two screens into the app_settings singleton would close it
properly; the PocketBase URL and credentials cannot follow, since they are
how the database is reached in the first place.

go build, go vet and go test ./... pass; the compose files parse and each
resolves to a single pb_data volume. Not verified: no Docker CLI here, so
neither image was built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:41:02 +02:00

249 lines
9.7 KiB
Docker

# syntax=docker/dockerfile:1
#
# All-in-one image: PocketBase + API Server + Web App in a single container.
#
# 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" -t drivervault-aio .
#
# Run it (all three services start together):
#
# docker run -d --name drivervault -p 80:80 -p 8070:8070 \
# -e PB_ADMIN_EMAIL=admin@example.com \
# -e PB_ADMIN_PASSWORD=change-me \
# -e DRIVERVAULT_SUPERADMIN_EMAIL=owner@example.com \
# -e DRIVERVAULT_SUPERADMIN_PASSWORD=change-me \
# -v drivervault_pb:/pb/pb_data \
# drivervault-aio
#
# Then: web app on http://host/ and PocketBase admin on http://host:8070/_/
# On first boot the API Server creates the collections and the super-admin.
# --- 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: 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 and the API Server. Only nginx stays root,
# because it binds port 80; supervisord drops to this user for the other two.
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.
COPY --from=api-build /out/api-server /usr/local/bin/api-server
COPY --from=web-build /app/dist /usr/share/nginx/html
# 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/ 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;
}
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 three processes and keeps them alive.
RUN cat > /etc/supervisord.conf <<'SUPERVISOR'
[supervisord]
nodaemon=true
user=root
pidfile=/run/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
; PocketBase: upsert the superuser (idempotent) then 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 '/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. 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 volume 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; 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.
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
# Required at runtime (no safe defaults): PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD.
# 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 ...`.
# pb_data holds everything that persists — the database, the uploads, and the
# server settings that used to sit in a file beside the binary. It is the only
# volume: the API Server keeps no state on disk. Both directories are
# pre-created and owned by app so a fresh named volume inherits that ownership.
RUN mkdir -p /pb/pb_data /app && chown -R app:app /pb /app
VOLUME /pb/pb_data
# 80 = Web App, 8070 = PocketBase admin, 8080 = API Server + embedded API panel
# (also the /ocpp/{serial} endpoint chargers dial into).
EXPOSE 80 8070 8080
# All three processes must answer, so a wedged component shows up in `docker ps`
# instead of a container that looks up while half of it is dead. start-period
# covers the first-boot schema bootstrap on a cold database.
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD 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"]