Charger control needs TLS, and the stack speaks plain HTTP, so the README said "terminate TLS in a reverse proxy" and left the operator to work out which four settings have to agree. This adds the proxy: a Caddy overlay that fronts the Web App BFF — which already carries /api/ and /ocpp/ — so browsers and chargers arrive at the same name and the certificate is issued on first boot. The four settings are derived from DV_DOMAIN, since one value getting typed right is better odds than four: OCPP_PUBLIC_URL, OCPP_REQUIRE_TLS back on, CORS, and TRUST_FORWARDED_PROTO on the Web App. That last one is the non-obvious one — without it the BFF overwrites Caddy's X-Forwarded-Proto with its own plaintext hop and the API Server rejects the charger it just told to connect over wss. The README's OCPP section was stale besides: it still sent chargers to the API Server port alone, from before the BFF carried that path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
5.9 KiB
Markdown
133 lines
5.9 KiB
Markdown
# DriverVault — Docker (multi-container stack)
|
|
|
|
Three containers — **PocketBase**, **API Server**, **Web App** — on one compose
|
|
network. This is the deployment to use unless you specifically want everything
|
|
in a single image; for that see [`../Docker-AIO`](../Docker-AIO).
|
|
|
|
```
|
|
Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─► PocketBase (:8070)
|
|
```
|
|
|
|
Only the Web App port is meant to be public. The API Server and the PocketBase
|
|
admin UI are published for convenience and, in the prod file, bound to
|
|
`127.0.0.1` by default.
|
|
|
|
| File | Use |
|
|
|---|---|
|
|
| `docker-compose.yml` | **builds from source** in this repo — for development and local testing |
|
|
| `docker-compose.prod.yml` | **pulls prebuilt images** from the registry — for deployment |
|
|
| `.env.example` / `.env.prod.example` | copy to `.env` for the matching compose file |
|
|
| `pocketbase/` | the PocketBase image (official release binary on alpine) |
|
|
|
|
## Run it
|
|
|
|
```bash
|
|
cd Docker
|
|
cp .env.example .env # then edit — PB_ADMIN_* have no safe defaults
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Production, from the registry:
|
|
|
|
```bash
|
|
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/_/`.
|
|
|
|
## First boot
|
|
|
|
Both steps are idempotent, so restarts and upgrades are safe:
|
|
|
|
1. **PocketBase** upserts its superuser from `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD`.
|
|
This is the only way to create the first superuser — the REST API cannot
|
|
bootstrap it. The API Server then authenticates with the same credentials.
|
|
2. **The API Server** creates any missing collections and reconciles existing
|
|
ones, then creates the first app `superadmin` from
|
|
`DRIVERVAULT_SUPERADMIN_EMAIL` / `_PASSWORD` if no such user exists.
|
|
|
|
> Leave `PB_BOOTSTRAP` at `true`, including across upgrades. A release can add
|
|
> collections or fields the server needs, and a stack that skipped the bootstrap
|
|
> never gets them. The API Server self-heals exactly one thing — `app_settings`,
|
|
> the collection holding the plugin settings, which it creates on demand because
|
|
> it cannot serve the plugin panel without it. Every other schema change still
|
|
> depends on this flag, so turn it off only for a database you know already
|
|
> matches the release you are running.
|
|
|
|
No manual `setup-pocketbase.mjs` step is needed here — the server runs the same
|
|
schema reconcile itself.
|
|
|
|
## Volumes
|
|
|
|
| Volume | Holds |
|
|
|---|---|
|
|
| `pb_data` | the PocketBase SQLite database and uploaded files — everything that persists |
|
|
|
|
One volume, because the API Server keeps no state on disk. Plugin enable-state
|
|
and global config live in the database like the rest of the settings, so backing
|
|
up `pb_data` backs up the whole stack. It defaults to a Docker-managed named
|
|
volume; set `PB_DATA` to an absolute host path in the prod file for a bind mount
|
|
instead. PocketBase runs as root, so a root-owned host directory is fine.
|
|
|
|
> One thing does **not** persist: the API Server panel's *Settings → PocketBase*
|
|
> and *Settings → Web App* screens apply immediately but only for the life of the
|
|
> container. Set `POCKETBASE_URL`, `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD`,
|
|
> `WEBAPP_URL` and `CORS_ALLOW_ORIGINS` in `.env` to change them permanently —
|
|
> in this stack the compose environment wins over anything the panel writes.
|
|
|
|
## Charger control (OCPP)
|
|
|
|
Chargers in own/proxy mode dial in to `/ocpp/{serial}`, authenticating with a
|
|
per-charger control token in an OCPP Basic-auth header. Two ports answer that
|
|
path: the API Server's own, and the Web App's, whose BFF proxies `/ocpp/`
|
|
through. The second one matters because it is the address the panel hands out —
|
|
the endpoint is derived from the host the panel itself was reached on, which is
|
|
the Web App, unless `OCPP_PUBLIC_URL` says otherwise.
|
|
|
|
A plaintext `ws://` puts the control token on the wire in the clear, so
|
|
`OCPP_REQUIRE_TLS` defaults to `true` and non-TLS connections are rejected.
|
|
`OCPP_REQUIRE_TLS=false` is for a trusted network you own end to end.
|
|
|
|
### With a public hostname and TLS
|
|
|
|
`docker-compose.tls.yml` adds Caddy in front of the stack: one hostname, a
|
|
certificate issued on first boot, and everything behind it spoken to over the
|
|
compose network. Browsers and chargers arrive at the same name.
|
|
|
|
```sh
|
|
# in .env
|
|
DV_DOMAIN=drivervault.example.com
|
|
DV_ACME_EMAIL=you@example.com
|
|
|
|
docker compose -f docker-compose.prod.yml -f docker-compose.tls.yml up -d
|
|
```
|
|
|
|
The overlay sets the rest for you: `OCPP_PUBLIC_URL=wss://$DV_DOMAIN`,
|
|
`OCPP_REQUIRE_TLS=true`, `CORS_ALLOW_ORIGINS=https://$DV_DOMAIN`, and
|
|
`TRUST_FORWARDED_PROTO=true` on the Web App so the BFF passes Caddy's
|
|
`X-Forwarded-Proto` to the API Server instead of overwriting it with its own
|
|
plaintext hop. Point the charger's OCPP backend at the endpoint the panel then
|
|
shows, with the control token as its authorization key.
|
|
|
|
Two things the overlay cannot arrange: `DV_DOMAIN` must resolve to the host from
|
|
the internet with ports 80 and 443 reaching it (Caddy needs `:80` for the ACME
|
|
challenge), and the charger must be able to resolve that name too — behind NAT
|
|
that usually means hairpin NAT or a split-DNS entry pointing it at the LAN
|
|
address.
|
|
|
|
Using a proxy you already run instead? Terminate TLS there, forward to the Web
|
|
App port, and set the same four variables by hand — the `X-Forwarded-Proto` one
|
|
included, or chargers will be rejected as insecure.
|
|
|
|
## Notes
|
|
|
|
- `docker-compose.yml` builds the API Server and Web App from `../API Server`
|
|
and `../Web App`, so run it from this directory with the repo checked out.
|
|
- The Web App's Vue bundle is built with an empty `VITE_API_BASE`, so the
|
|
browser uses same-origin `/api` and the BFF proxies it — no CORS in play.
|
|
- `CORS_ALLOW_ORIGINS` therefore only matters if a browser calls the API Server
|
|
directly. Native mobile apps are not subject to CORS at all.
|