Skip to content

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.

  • Phase 1 (host prep) complete — Docker + Compose installed, the host has a pangolin external 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 the pangolin network.
  • The expresscharge/steve repository checked out on the host. The default branch expressync-preauth is what you want.
  • Three secrets generated and recorded somewhere safe:
    • MariaDB password for the steve user
    • Admin web UI basic-auth password
    • REST API token value (used as the webapi.value header)
  1. Create the pangolin network if it doesn't exist

    SteVe’s Compose file attaches the app container to an external network named pangolin. If you haven’t already created it during Phase 2, do so now:

    Terminal window
    docker network ls | grep pangolin || docker network create pangolin
  2. Copy the env-file examples

    Both env files are gitignored. Copy the committed examples:

    Terminal window
    cd steve
    cp docker-compose.app.env.example docker-compose.app.env
    cp docker-compose.mariadb.env.example docker-compose.mariadb.env
  3. Fill in secrets

    Edit docker-compose.app.env and set:

    • DB_PASSWORD required — the MariaDB password for the steve user.
    • AUTH_PASSWORD required — admin web UI basic-auth password.
    • WEBAPI_VALUE required — REST API token. ExpresSync will send this as a header on every call.

    Edit docker-compose.mariadb.env and set MYSQL_PASSWORD to the same value as DB_PASSWORD. These are the same credential consumed by two different processes; if they drift the app will fail to connect on first boot.

  4. Confirm the data directory

    The Compose file bind-mounts /data/ocpp/mariadb on the host into the MariaDB container. Create it and make sure the directory exists with appropriate permissions:

    Terminal window
    sudo mkdir -p /data/ocpp/mariadb

    The :Z SELinux relabel flag is already set in the Compose file; on non-SELinux hosts it’s a no-op.

  5. 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 build

    The build pulls JDK 21, runs ./mvnw package, and produces a runnable war. Expect this to take several minutes on first build.

  6. Start the stack

    Terminal window
    docker compose up -d
    docker compose logs app -f

    Boot takes about 30 seconds. Flyway migrations run before Jetty binds its listener; you’ll see the schema migrations scroll past, then a Started SteveAppContext line. Ctrl-C to detach from logs (the containers keep running).

  7. Wire the public hostname

    In your Pangolin admin UI, create a resource that proxies the public hostname (for example ocpp.example.com) to the app container on the pangolin network, port 8180. 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.

The three runtime secrets are injected at JVM startup via -D properties, overriding the placeholder values committed in application-docker.properties.

VariableDefaultRequiredSource file
DB_PASSWORD(none)yesdocker-compose.app.env
AUTH_PASSWORD(none)yesdocker-compose.app.env
WEBAPI_VALUE(none)yesdocker-compose.app.env
MYSQL_PASSWORD(none)yes — must equal DB_PASSWORDdocker-compose.mariadb.env
MYSQL_ROOT_PASSWORD(none)yesdocker-compose.mariadb.env
MYSQL_USERstevenodocker-compose.mariadb.env
MYSQL_DATABASEstevedbnodocker-compose.mariadb.env

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:

Terminal window
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:

Terminal window
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/unknown

A 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.

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.

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.