One hostname a charger can be told about and actually reach

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>
This commit is contained in:
tajniak81
2026-09-01 15:36:56 +02:00
co-authored by Claude Opus 5
parent 9f5c8dc49a
commit e9a82a1cca
5 changed files with 141 additions and 9 deletions
+40 -9
View File
@@ -80,16 +80,47 @@ instead. PocketBase runs as root, so a root-owned host directory is fine.
## Charger control (OCPP)
Chargers in own/proxy mode dial in to `/ocpp/{serial}` **on the API Server
port**, authenticating with a per-charger control token in an OCPP Basic-auth
header. A plaintext `ws://` would put that token on the wire in the clear, so
`OCPP_REQUIRE_TLS` defaults to `true` and non-TLS connections are rejected.
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.
This stack serves plain HTTP, so to actually use charger control you need to
terminate TLS in a reverse proxy in front of it and set `OCPP_PUBLIC_URL` to the
public `wss://` base (behind a proxy, deriving it from request headers is
unreliable). `OCPP_REQUIRE_TLS=false` is for trusted networks only. You will
also need `API_BIND` set so the proxy can reach the port.
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