From 01d82ebda92e22f0ebea988a1dbbbbc25cbd986d Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:46:01 +0200 Subject: [PATCH] Docker: persist API Server state, wire up OCPP, drop legacy env names Audited every Dockerfile, compose file and .env.example against the code they deploy. Four things had drifted: Persistence. The API Server writes plugins.json and rewrites .env (the panel's retarget-PocketBase flow) relative to its working directory, which was a root-owned /app while the process runs as the app user - so both writes failed, and no volume was declared to keep them anyway. The binary moves to /usr/local/bin and the working directory becomes a /data volume owned by app. The AIO image gets the same via directory=/data on its supervisord program. OCPP. Charger control was undeployable: OCPP_REQUIRE_TLS defaults to true and appeared in no Docker file, so a charger dialling the plain-HTTP /ocpp/{serial} was rejected with nothing explaining why. Both OCPP vars are now threaded through the compose files and env examples, with the reasoning (the per-charger control token rides in a Basic-auth header). CORS. API Server/docker-compose.yml defaulted to localhost:5173, the Vite dev port, where every other file uses 8090. Env names. .env.example has called PB_URL/PB_ADMIN_*/PORT legacy for a while, but the Docker layer still used them. Container-side names are now POCKETBASE_*/API_ADDR; the .env keys operators set stay PB_ADMIN_* so existing .env files keep working. Left alone deliberately: alpine:latest stays unpinned (cannot verify current tags or test the build from here), and the golang/node bases already match go.mod and Vite 8's floor. Validated as YAML only - there is no Docker CLI on this machine, so no image was built and the /data ownership fix follows standard volume semantics rather than an observed run. Co-Authored-By: Claude Opus 4.8 --- API Server/.env.example | 15 +++++++++++++++ API Server/Dockerfile | 20 +++++++++++++++----- API Server/docker-compose.yml | 27 ++++++++++++++++++++------- Docker AIO/.env.example | 10 ++++++++++ Docker AIO/.env.prod.example | 19 +++++++++++++++++-- Docker AIO/Dockerfile | 24 +++++++++++++++++++----- Docker AIO/docker-compose.prod.yml | 14 ++++++++++++-- Docker AIO/docker-compose.yml | 11 ++++++++++- Docker/.env.example | 10 ++++++++++ Docker/.env.prod.example | 21 ++++++++++++++++++--- Docker/docker-compose.prod.yml | 22 +++++++++++++++++----- Docker/docker-compose.yml | 22 +++++++++++++++++----- 12 files changed, 180 insertions(+), 35 deletions(-) diff --git a/API Server/.env.example b/API Server/.env.example index 54ba636..7547949 100644 --- a/API Server/.env.example +++ b/API Server/.env.example @@ -31,8 +31,23 @@ WEBAPP_URL=http://localhost:8090 AUTH_USERS_COLLECTION=users # Local JSON store for plugin enable-state + config (default: plugins.json). +# Relative paths resolve against the working directory, so in Docker this points +# at the mounted volume (see the Dockerfile) rather than the image layer. PLUGINS_FILE=plugins.json +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on this server, authenticating with OCPP Basic auth +# (serial + per-charger control token). A plaintext ws:// would carry that token +# in the clear, so connections that did not arrive over TLS are rejected — +# disable only for local dev on a trusted network. +OCPP_REQUIRE_TLS=true + +# The canonical ws(s):// base an operator points the charger at. Set this when +# the server sits behind a reverse proxy, where deriving the URL from request +# headers is unreliable. Empty = derive it. +OCPP_PUBLIC_URL= + # --- Legacy names ----------------------------------------------------------- # PB_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, PORT and CORS_ORIGINS are still # honoured for older deployments; the POCKETBASE_*/API_ADDR names above win when diff --git a/API Server/Dockerfile b/API Server/Dockerfile index daf187f..c9a5b25 100644 --- a/API Server/Dockerfile +++ b/API Server/Dockerfile @@ -28,13 +28,23 @@ RUN apk add --no-cache ca-certificates tzdata # Run as an unprivileged user. RUN addgroup -S app && adduser -S -G app app -WORKDIR /app -COPY --from=build /out/api-server /app/api-server +COPY --from=build /out/api-server /usr/local/bin/api-server + +# The server writes two files relative to its working directory: plugins.json +# (plugin enable-state + config) and .env, which the panel rewrites when a +# superadmin retargets the PocketBase connection. Both must therefore live on a +# writable, persistent path — hence /data, owned by the unprivileged user and +# declared as a volume. A named volume mounted here inherits this ownership. +RUN mkdir -p /data && chown app:app /data +WORKDIR /data +VOLUME /data # Config comes entirely from environment variables (see .env.example). -# PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD are required at startup. -ENV PORT=8080 +# POCKETBASE_ADMIN_EMAIL / _PASSWORD are optional at startup: without them the +# server still runs and a superadmin can configure the connection from the panel. +ENV API_ADDR=:8080 \ + PLUGINS_FILE=/data/plugins.json EXPOSE 8080 USER app -ENTRYPOINT ["/app/api-server"] +ENTRYPOINT ["/usr/local/bin/api-server"] diff --git a/API Server/docker-compose.yml b/API Server/docker-compose.yml index 4e1dabf..3f028c4 100644 --- a/API Server/docker-compose.yml +++ b/API Server/docker-compose.yml @@ -7,16 +7,29 @@ services: container_name: drivervault-api restart: unless-stopped ports: - - "${PORT:-8080}:8080" + - "${API_PORT:-8080}:8080" environment: # Container always listens on 8080; map the host port above. - PORT: "8080" + API_ADDR: ":8080" # External PocketBase instance (all DB access goes through this server). - PB_URL: "${PB_URL:-http://10.2.1.10:8027}" + POCKETBASE_URL: "${PB_URL:-http://10.2.1.10:8027}" # Superuser service account. Leave unset and the server still starts — a # superadmin can configure it from the panel; management endpoints 503 until then. - PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" - PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" - # Browser origins allowed by CORS (native apps are exempt). - CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:5173}" + POCKETBASE_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" + POCKETBASE_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" + # Browser origins allowed by CORS (native apps are exempt). Point this at + # the Web App origin; add http://localhost:5173 when running Vite in dev. + CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" + # OCPP charger control (Anker Solix). Chargers are rejected unless they + # connect over TLS; set false only when terminating TLS elsewhere or for + # local dev on a trusted network. OCPP_PUBLIC_URL overrides the ws(s):// + # base derived from request headers (set it when behind a reverse proxy). + OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" + OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" + volumes: + # Holds plugins.json and the .env the panel writes back — see Dockerfile. + - api_data:/data + +volumes: + api_data: diff --git a/Docker AIO/.env.example b/Docker AIO/.env.example index 04b9cf4..02333ea 100644 --- a/Docker AIO/.env.example +++ b/Docker AIO/.env.example @@ -18,6 +18,16 @@ PB_BOOTSTRAP=true # Allowed CORS origin(s) — match your web origin / WEB_PORT. CORS_ALLOW_ORIGINS=http://localhost:8090 +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. This image serves plain HTTP: +# either terminate TLS in front of it and set OCPP_PUBLIC_URL to the public +# wss:// base, or set OCPP_REQUIRE_TLS=false on a trusted network. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + # --- Host port mappings (optional; defaults shown) -------------------------- WEB_PORT=8090 PB_PORT=8070 diff --git a/Docker AIO/.env.prod.example b/Docker AIO/.env.prod.example index e9d40ea..8e1e2e9 100644 --- a/Docker AIO/.env.prod.example +++ b/Docker AIO/.env.prod.example @@ -24,12 +24,27 @@ PB_BOOTSTRAP=true # Allowed CORS origin(s) — match your public web URL / WEB_PORT. CORS_ALLOW_ORIGINS=http://localhost:8090 +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. This image serves plain HTTP, so +# terminate TLS in a reverse proxy in front of it and set OCPP_PUBLIC_URL to the +# public wss:// base the charger should be pointed at. Turning the check off is +# for trusted networks only. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + # --- Host port mappings (optional; defaults shown) -------------------------- WEB_PORT=8090 PB_PORT=8070 API_PORT=8080 # --- Storage ----------------------------------------------------------------- -# Default is a Docker-managed named volume ("pb_data"). Set PB_DATA to an -# absolute host path for a bind mount, e.g. PB_DATA=/srv/drivervault/pb_data +# Defaults are Docker-managed named volumes. Set either to an absolute host path +# for a bind mount, e.g. PB_DATA=/srv/drivervault/pb_data. +# PB_DATA — the PocketBase database and uploads. +# API_DATA — the API Server's plugins.json and the .env the panel writes back +# when a superadmin retargets the PocketBase connection. PB_DATA=pb_data +API_DATA=api_data diff --git a/Docker AIO/Dockerfile b/Docker AIO/Dockerfile index d02868b..fce894c 100644 --- a/Docker AIO/Dockerfile +++ b/Docker AIO/Dockerfile @@ -15,6 +15,7 @@ # -e DRIVERVAULT_SUPERADMIN_EMAIL=owner@example.com \ # -e DRIVERVAULT_SUPERADMIN_PASSWORD=change-me \ # -v drivervault_pb:/pb/pb_data \ +# -v drivervault_api:/data \ # drivervault-aio # # Then: web app on http://host/ and PocketBase admin on http://host:8070/_/ @@ -127,8 +128,11 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -; API Server: wait for PocketBase to be healthy, then start. +; API Server: wait for PocketBase to be healthy, then start. It runs from /data +; because it writes plugins.json and the panel's .env relative to its working +; directory, and /data is the volume that keeps them across container recreates. [program:api-server] +directory=/data 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 @@ -150,18 +154,28 @@ stderr_logfile_maxbytes=0 SUPERVISOR # API Server config: everything is local to this container. -ENV PORT=8080 \ - PB_URL=http://127.0.0.1:8070 \ +ENV API_ADDR=:8080 \ + POCKETBASE_URL=http://127.0.0.1:8070 \ CORS_ALLOW_ORIGINS=http://localhost:8090 \ - AUTH_USERS_COLLECTION=users + AUTH_USERS_COLLECTION=users \ + PLUGINS_FILE=/data/plugins.json # 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. +# 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 ...`. +RUN mkdir -p /data +# pb_data holds the database; /data holds the API Server's plugins.json and the +# .env the panel rewrites when a superadmin retargets PocketBase. VOLUME /pb/pb_data -# 80 = Web App, 8070 = PocketBase admin, 8080 = API Server + embedded API panel. +VOLUME /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 CMD ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/Docker AIO/docker-compose.prod.yml b/Docker AIO/docker-compose.prod.yml index a1a9fcb..bf90f11 100644 --- a/Docker AIO/docker-compose.prod.yml +++ b/Docker AIO/docker-compose.prod.yml @@ -29,13 +29,23 @@ services: DRIVERVAULT_SUPERADMIN_EMAIL: "${DRIVERVAULT_SUPERADMIN_EMAIL:-}" DRIVERVAULT_SUPERADMIN_PASSWORD: "${DRIVERVAULT_SUPERADMIN_PASSWORD:-}" DRIVERVAULT_SUPERADMIN_NAME: "${DRIVERVAULT_SUPERADMIN_NAME:-Administrator}" + # OCPP charger control (Anker Solix). This image serves plain HTTP, so a + # charger can only connect when TLS is terminated in front of it (set + # OCPP_PUBLIC_URL to the public wss:// base) — or, on a trusted network, + # with OCPP_REQUIRE_TLS=false. + OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" + OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" ports: - "${WEB_PORT:-8090}:80" # Web App - "${PB_PORT:-8070}:8070" # PocketBase admin UI / API - - "${API_PORT:-8080}:8080" # API Server + embedded API web panel (root /) + - "${API_PORT:-8080}:8080" # API Server + panel (root /) + /ocpp/{serial} volumes: - # Named volume by default; set PB_DATA to a host path in .env for a bind mount. + # Named volumes by default; set PB_DATA / API_DATA to host paths in .env + # for bind mounts. - "${PB_DATA:-pb_data}:/pb/pb_data" + # plugins.json + the .env the panel writes back. + - "${API_DATA:-api_data}:/data" volumes: pb_data: + api_data: diff --git a/Docker AIO/docker-compose.yml b/Docker AIO/docker-compose.yml index 359e690..b5d0f1b 100644 --- a/Docker AIO/docker-compose.yml +++ b/Docker AIO/docker-compose.yml @@ -30,12 +30,21 @@ services: DRIVERVAULT_SUPERADMIN_EMAIL: "${DRIVERVAULT_SUPERADMIN_EMAIL:-}" DRIVERVAULT_SUPERADMIN_PASSWORD: "${DRIVERVAULT_SUPERADMIN_PASSWORD:-}" DRIVERVAULT_SUPERADMIN_NAME: "${DRIVERVAULT_SUPERADMIN_NAME:-Administrator}" + # OCPP charger control (Anker Solix). This image serves plain HTTP, so a + # charger can only connect when TLS is terminated in front of it (set + # OCPP_PUBLIC_URL to the public wss:// base) — or, on a trusted network, + # with OCPP_REQUIRE_TLS=false. + OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" + OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" ports: - "${WEB_PORT:-8090}:80" # Web App - "${PB_PORT:-8070}:8070" # PocketBase admin UI / API - - "${API_PORT:-8080}:8080" # API Server + embedded API web panel (root /) + - "${API_PORT:-8080}:8080" # API Server + panel (root /) + /ocpp/{serial} volumes: - pb_data:/pb/pb_data + # plugins.json + the .env the panel writes back. + - api_data:/data volumes: pb_data: + api_data: diff --git a/Docker/.env.example b/Docker/.env.example index a284c86..4e6addd 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -10,6 +10,16 @@ PB_ADMIN_PASSWORD=change-me-long-password CORS_ALLOW_ORIGINS=http://localhost:8090 AUTH_USERS_COLLECTION=users +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. This dev stack serves plain +# HTTP: either terminate TLS in front of it and set OCPP_PUBLIC_URL to the +# public wss:// base, or set OCPP_REQUIRE_TLS=false on a trusted network. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + # --- Host port mappings (optional; defaults shown) -------------------------- PB_PORT=8070 API_PORT=8080 diff --git a/Docker/.env.prod.example b/Docker/.env.prod.example index 1b52267..f4afce5 100644 --- a/Docker/.env.prod.example +++ b/Docker/.env.prod.example @@ -32,6 +32,18 @@ PB_BOOTSTRAP=true CORS_ALLOW_ORIGINS=http://localhost:8090 AUTH_USERS_COLLECTION=users +# --- EV charging control (Anker Solix, OCPP) --------------------------------- +# Only relevant when a charger is set to own/proxy control mode. The charger +# dials in to /ocpp/{serial} on the API Server port, carrying its control token +# in an OCPP Basic-auth header — which a plaintext ws:// would expose, so +# non-TLS connections are rejected by default. Keep the default and terminate +# TLS in a reverse proxy in front of this stack, setting OCPP_PUBLIC_URL to the +# public wss:// base the charger should be pointed at (deriving it from request +# headers is unreliable behind a proxy). Turning the check off is for trusted +# networks only. +OCPP_REQUIRE_TLS=true +OCPP_PUBLIC_URL= + # --- Ports ------------------------------------------------------------------- # WEB_PORT is the public front door (bound on all interfaces). WEB_PORT=8090 @@ -43,7 +55,10 @@ API_PORT=8080 API_BIND=127.0.0.1 # --- Storage ----------------------------------------------------------------- -# Default is a Docker-managed named volume ("pb_data"). To store the database on -# a host path instead, set PB_DATA to an absolute path, e.g.: -# PB_DATA=/srv/drivervault/pb_data +# Defaults are Docker-managed named volumes. To store either on a host path +# instead, set it to an absolute path, e.g. PB_DATA=/srv/drivervault/pb_data. +# PB_DATA — the PocketBase database and uploads. +# API_DATA — the API Server's plugins.json and the .env the panel writes back +# when a superadmin retargets the PocketBase connection. PB_DATA=pb_data +API_DATA=api_data diff --git a/Docker/docker-compose.prod.yml b/Docker/docker-compose.prod.yml index 49d9c41..b21d04e 100644 --- a/Docker/docker-compose.prod.yml +++ b/Docker/docker-compose.prod.yml @@ -48,11 +48,11 @@ services: pocketbase: condition: service_healthy environment: - PORT: "8080" + API_ADDR: ":8080" # Reach PocketBase by its service name on the internal network. - PB_URL: "http://pocketbase:8070" - PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" - PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" + POCKETBASE_URL: "http://pocketbase:8070" + POCKETBASE_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" + POCKETBASE_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" # Schema + super-admin bootstrap (idempotent). Set PB_BOOTSTRAP=false to @@ -61,10 +61,21 @@ services: DRIVERVAULT_SUPERADMIN_EMAIL: "${DRIVERVAULT_SUPERADMIN_EMAIL:-}" DRIVERVAULT_SUPERADMIN_PASSWORD: "${DRIVERVAULT_SUPERADMIN_PASSWORD:-}" DRIVERVAULT_SUPERADMIN_NAME: "${DRIVERVAULT_SUPERADMIN_NAME:-Administrator}" + # OCPP charger control (Anker Solix). Chargers are rejected unless they + # reach the server over TLS. Behind a TLS-terminating reverse proxy, set + # OCPP_PUBLIC_URL to the public wss:// base and API_BIND so the proxy can + # reach this port; only drop OCPP_REQUIRE_TLS on a trusted network. + OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" + OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" ports: # Localhost-only by default (the Web App reaches it over the internal - # network). Set API_BIND=0.0.0.0 to expose the API panel on the network. + # network). Set API_BIND=0.0.0.0 to expose the API panel — and the + # /ocpp/{serial} endpoint chargers dial into — on the network. - "${API_BIND:-127.0.0.1}:${API_PORT:-8080}:8080" + volumes: + # plugins.json + the .env the panel writes back — see the API Server + # Dockerfile. Without this, plugin state is lost on container recreate. + - "${API_DATA:-api_data}:/data" web-app: image: "${WEB_IMAGE:-10.2.1.10:5500/admin/drivervault-web-app:latest}" @@ -81,3 +92,4 @@ services: volumes: pb_data: + api_data: diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index 000858a..613f794 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -37,19 +37,30 @@ services: pocketbase: condition: service_healthy environment: - PORT: "8080" + API_ADDR: ":8080" # Reach PocketBase by its service name on the internal network. - PB_URL: "http://pocketbase:8070" - PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" - PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" + POCKETBASE_URL: "http://pocketbase:8070" + POCKETBASE_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" + POCKETBASE_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" # Same-origin requests go through the Web App BFF, so CORS is only needed # if the browser ever calls the API Server directly. Default to the web origin. CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8090}" AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" + # OCPP charger control (Anker Solix). Chargers are rejected unless they + # connect over TLS; set OCPP_REQUIRE_TLS=false in .env only when TLS is + # terminated in front of this stack or for local dev on a trusted network. + OCPP_REQUIRE_TLS: "${OCPP_REQUIRE_TLS:-true}" + OCPP_PUBLIC_URL: "${OCPP_PUBLIC_URL:-}" ports: # Optional direct access to the API Server (and its panel at /); the Web - # App reaches it over the internal network, not this host port. + # App reaches it over the internal network, not this host port. Chargers + # dialling /ocpp/{serial} also arrive here. - "${API_PORT:-8080}:8080" + volumes: + # plugins.json + the .env the panel writes back — see the API Server + # Dockerfile. Without this, plugin state is lost when the container is + # recreated. + - api_data:/data web-app: build: @@ -70,3 +81,4 @@ services: volumes: pb_data: + api_data: