The all-in-one image builds from the project root, and Docker only reads .dockerignore from the context root — so the ones under "API Server" and "Web App" never applied to it and every AIO build shipped the whole tree, "Phone App/build" included. A root .dockerignore allow-lists the paths that build actually copies. The dev split stack passed neither PB_BOOTSTRAP nor the SUPERADMIN vars, so it created the schema and then no user to log in with. It passes them now, and .env.example says so. WEBAPP_URL was never set anywhere, leaving the panel status page probing localhost:8090 — itself — and always reporting the Web App as down. Each compose file now points it at wherever the Web App really is, and the BFF grew a real /healthz instead of letting the SPA fallback answer probes with index.html and look healthy no matter what. In the AIO, PocketBase and the API Server drop to an unprivileged user; only nginx stays root to bind :80. The entrypoint takes ownership of the two volumes first, so data written by the old root-only image stays writable. All three images carry a HEALTHCHECK, every compose file declares one too (so depends_on still gates against an older pulled image), and web-app waits for the API Server to be serving rather than merely started. Also: pinned alpine/golang/node and PocketBase 0.39.11, so a rebuild months from now produces the same image; nginx forwards WebSocket upgrades instead of stripping them, with the map in http.d where Alpine actually reads it; and a .gitattributes keeps entrypoint.sh on LF, because a CRLF shebang from a Windows clone fails at container start with "no such file or directory". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.0 KiB
DriverVault — Docker AIO (all-in-one image)
PocketBase + API Server + Web App in a single container, supervised by
supervisord with nginx serving the SPA and proxying /api/ to the API Server
on localhost. One image, one volume set, no compose network — the simplest way
to stand DriverVault up on a single host.
Prefer the three-container stack in ../Docker when you want to
scale, upgrade or restart the pieces independently.
:80 nginx ─► SPA, and /api/ ─► API Server on 127.0.0.1:8080 ─► PocketBase on :8070
| File | Use |
|---|---|
Dockerfile |
the all-in-one image (build context must be the repo root) |
docker-compose.yml |
builds from source — for development and local testing |
docker-compose.prod.yml |
pulls the prebuilt image from the registry |
.env.example / .env.prod.example |
copy to .env for the matching compose file |
Run it
cd "Docker-AIO"
cp .env.example .env # then edit — PB_ADMIN_* have no safe defaults
docker compose up -d --build
Production, from the registry:
cp .env.prod.example .env # then edit
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
Then: web app on http://host:8090/, the API Server's superadmin panel on
http://host:8080/, PocketBase admin on http://host:8070/_/.
Note the port mapping: inside the container the web app is on 80, published
as WEB_PORT (8090 by default) to line up with the other deployment.
Building by hand
The build context must be the repo root so the Dockerfile can reach both
API Server/ and Web App/:
docker build -f "Docker-AIO/Dockerfile" -t drivervault-aio .
Build args: VITE_API_BASE (leave empty so the bundle uses same-origin /api)
and PB_VERSION, which the Dockerfile already pins. Override it to build a
different PocketBase; set it to an empty string to resolve the latest release at
build time instead.
First boot
Identical to the multi-container stack, and idempotent:
- PocketBase upserts its superuser from
PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD. - The API Server waits for PocketBase to report healthy, then creates any
missing collections, reconciles existing ones, and creates the first app
superadminfromDRIVERVAULT_SUPERADMIN_EMAIL/_PASSWORD. SetPB_BOOTSTRAP=falseto skip once the database is established.
Volumes
| Volume | Holds |
|---|---|
/pb/pb_data |
the PocketBase SQLite database and uploaded files |
/data |
the API Server's plugins.json, and the .env the panel rewrites when a superadmin retargets the PocketBase connection |
Both default to Docker-managed named volumes; set PB_DATA / API_DATA to
absolute host paths in the prod file for bind mounts.
Charger control (OCPP)
Chargers in own/proxy mode dial in to /ocpp/{serial} on the API Server port
(8080) — not through nginx — authenticating with a per-charger control token
in an OCPP Basic-auth header. Because a plaintext ws:// would expose that
token, OCPP_REQUIRE_TLS defaults to true.
This image serves plain HTTP, so charger control needs TLS terminated in front
of it, with OCPP_PUBLIC_URL set to the public wss:// base. Only drop
OCPP_REQUIRE_TLS on a trusted network.
Caveats
- PocketBase and the API Server run as the unprivileged
appuser; only nginx stays root, because it binds port 80. A crash ofsupervisordstill takes all three services down together — that is the trade for the simplicity. - Logs from all three processes are interleaved on the container's stdout/stderr
(
docker logs drivervault-aio). PB_VERSIONis pinned in the Dockerfile so two builds of the same source agree. Setting it to an empty string restores the old behaviour of resolving the latest release at build time, which also costs an unauthenticated GitHub API call and is subject to that 60/hour per-IP rate limit.- The container reports health once all three processes answer their probes, so
a wedged component shows up in
docker psrather than looking up.