Phase 4 — Deploy SteVe (OCPP backend)
SteVe is the OCPP central system: it terminates the WebSocket from each
physical charge point, persists transactions and meter readings to
MariaDB, and exposes a REST API plus an admin web UI. ExpresSync uses
SteVe’s REST API for remote-start and pre-authorization, and receives
push notifications from SteVe via the HttpMeterValueHook outbound
webhook.
This phase builds the ExpressCharge fork of SteVe (expressync-preauth
branch), brings up MariaDB and the SteVe app via Docker Compose, and
verifies that the admin UI and REST API are reachable.
Prerequisites
Section titled “Prerequisites”- Phase 1 (host prep) complete — Docker + Compose installed, the host
has a
pangolinexternal network created. - Phase 2 (Pangolin tunnel) complete — you have a public hostname (for
example
ocpp.example.com) that will route to the SteVe container on thepangolinnetwork. - The
expresscharge/steverepository checked out on the host. The default branchexpressync-preauthis what you want. - Three secrets generated and recorded somewhere safe:
- MariaDB password for the
steveuser - Admin web UI basic-auth password
- REST API token value (used as the
webapi.valueheader)
- MariaDB password for the
-
Create the pangolin network if it doesn't exist
SteVe’s Compose file attaches the
appcontainer to an external network namedpangolin. If you haven’t already created it during Phase 2, do so now:Terminal window docker network ls | grep pangolin || docker network create pangolin -
Copy the env-file examples
Both env files are gitignored. Copy the committed examples:
Terminal window cd stevecp docker-compose.app.env.example docker-compose.app.envcp docker-compose.mariadb.env.example docker-compose.mariadb.env -
Fill in secrets
Edit
docker-compose.app.envand set:-
DB_PASSWORDrequired — the MariaDB password for thesteveuser. -
AUTH_PASSWORDrequired — admin web UI basic-auth password. -
WEBAPI_VALUErequired — REST API token. ExpresSync will send this as a header on every call.
Edit
docker-compose.mariadb.envand setMYSQL_PASSWORDto the same value asDB_PASSWORD. These are the same credential consumed by two different processes; if they drift the app will fail to connect on first boot. -
-
Confirm the data directory
The Compose file bind-mounts
/data/ocpp/mariadbon the host into the MariaDB container. Create it and make sure the directory exists with appropriate permissions:Terminal window sudo mkdir -p /data/ocpp/mariadbThe
:ZSELinux relabel flag is already set in the Compose file; on non-SELinux hosts it’s a no-op. -
Build the image
The SteVe image bakes the Maven build artifacts. There is no live-reload — source changes require a rebuild.
Terminal window docker compose buildThe build pulls JDK 21, runs
./mvnw package, and produces a runnablewar. Expect this to take several minutes on first build. -
Start the stack
Terminal window docker compose up -ddocker compose logs app -fBoot takes about 30 seconds. Flyway migrations run before Jetty binds its listener; you’ll see the schema migrations scroll past, then a
Started SteveAppContextline.Ctrl-Cto detach from logs (the containers keep running). -
Wire the public hostname
In your Pangolin admin UI, create a resource that proxies the public hostname (for example
ocpp.example.com) to theappcontainer on thepangolinnetwork, port8180. Both HTTP (for the admin UI and REST API) and WebSocket (for OCPP/J) traffic must be allowed; OCPP/J upgrades the same HTTP connection to a WebSocket.
Configure environment variables
Section titled “Configure environment variables”The three runtime secrets are injected at JVM startup via -D
properties, overriding the placeholder values committed in
application-docker.properties.
| Variable | Default | Required | Source file |
|---|---|---|---|
DB_PASSWORD | (none) | yes | docker-compose.app.env |
AUTH_PASSWORD | (none) | yes | docker-compose.app.env |
WEBAPI_VALUE | (none) | yes | docker-compose.app.env |
MYSQL_PASSWORD | (none) | yes — must equal DB_PASSWORD | docker-compose.mariadb.env |
MYSQL_ROOT_PASSWORD | (none) | yes | docker-compose.mariadb.env |
MYSQL_USER | steve | no | docker-compose.mariadb.env |
MYSQL_DATABASE | stevedb | no | docker-compose.mariadb.env |
Verify
Section titled “Verify”Admin UI reachable. Open https://ocpp.example.com/steve/manager
in a browser. You should be prompted for basic auth; the username is
admin and the password is your AUTH_PASSWORD. After signing in,
the Dashboard should render with zero charge points connected.
REST API reachable. From any machine that can reach the public hostname:
curl -i https://ocpp.example.com/steve/api/v1/chargeboxes \ -H "Authorization: Bearer $WEBAPI_VALUE"You should get 200 OK and an empty JSON array. If you get 401, the
WEBAPI_VALUE you sent doesn’t match what’s in the env file.
OCPP endpoint reachable. A real charge point will connect to:
wss://ocpp.example.com/steve/websocket/CentralSystemService/<chargeBoxId>You won’t be able to test this end-to-end until you’ve registered a ChargeBox in the admin UI (Phase 6 covers that). For now, confirm the WebSocket upgrade path is reachable:
curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \ -H "Sec-WebSocket-Protocol: ocpp1.6" \ https://ocpp.example.com/steve/websocket/CentralSystemService/unknownA 404 here is expected (the chargeBoxId isn’t registered) but a
404 from SteVe itself, not from Pangolin or nginx, confirms the
upgrade path works. A 502 or HTML error page means traffic isn’t
reaching SteVe.
If something goes wrong
Section titled “If something goes wrong”Access denied for user 'steve'@'…' in the app logs. Your
DB_PASSWORD and MYSQL_PASSWORD don’t match, or MariaDB’s data
volume was initialized with a different password on a prior run.
Either fix the env files, or — if this is a fresh install with no
real data — stop the stack, remove /data/ocpp/mariadb/*, and
docker compose up -d again so MariaDB re-initializes with the new
password.
Admin UI returns 401 no matter what password you enter.
The container is using the committed placeholder, not your
AUTH_PASSWORD. Confirm docker-compose.app.env is in the same
directory as docker-compose.yml, has no export prefixes, and
that docker compose config shows your env var in the resolved
configuration. Restart the app container after fixing.
docker compose build fails on Flyway migrations. The build
runs Flyway against MariaDB using BUILD_DB_PASSWORD. If you’ve
rotated the password without rebuilding, the bake step can’t connect.
Run docker compose build --no-cache and confirm the env file is
present.
WebSocket connections from charge points immediately disconnect.
Almost always a Pangolin / reverse-proxy config issue — confirm that
the resource forwards Upgrade and Connection headers and doesn’t
buffer the response. SteVe itself logs every connection attempt in
logs/; tail those to confirm the upgrade is reaching the app.
Next phase
Section titled “Next phase”Continue to Phase 5 — Deploy ExpresSync (web app), which deploys the Polaris Express web application and points it at the SteVe REST API you just stood up.