Ready-to-run Docker images for SteVe, the open-source OCPP Central System (CSMS).
ghcr.io/juherr/steve
Unofficial and community-maintained. Not affiliated with the SteVe project.
Upstream publishes no official image, and its docker-compose setup
recompiles the application every time the container starts — mvnw clean package sits in the CMD. That means Maven and a reachable database are
required on every restart, and several minutes of downtime each time.
Here the .war is compiled at build time, from a pinned upstream release
tag. Starting the container just starts the application.
The image is also self-migrating. SteVe applies its Flyway migrations during
the Maven build, against the code-generation database — which is thrown away
here. So the runtime database starts empty, and the entrypoint replays the
migrations against it on startup, using SteVe's exact Flyway configuration. This
is idempotent: on later restarts Flyway is a no-op. The migration scripts are
copied from the very same clone that produced the .war, so schema and binary
cannot drift apart.
One tag per upstream release: steve-<X.Y.Z> names SteVe release
steve-X.Y.Z. There is deliberately no latest and no per-commit tag.
The image currently runs on Eclipse Temurin 25 (JRE) — a build detail, not part of the tag. A JRE update republishes the same tag with a new digest.
Pin by digest — and you should. The digest is what Docker actually resolves, so the tag alongside it is documentation: a moving tag cannot change what you run, and it keeps version-tracking tools pointed at something still being republished.
docker buildx imagetools inspect ghcr.io/juherr/steve:steve-3.14.1image: ghcr.io/juherr/steve:steve-3.14.1@sha256:<digest>The exact JRE of an image you already hold is readable from it:
docker run --rm --entrypoint java ghcr.io/juherr/steve:steve-3.14.1 -versiondocker pull ghcr.io/juherr/steve:steve-3.14.1Minimal Compose setup:
services:
steve:
image: ghcr.io/juherr/steve:steve-3.14.1
restart: unless-stopped
depends_on:
steve-db:
condition: service_healthy
environment:
- DB_IP=steve-db
- DB_PASSWORD=<your-db-password>
- AUTH_USER=<your-admin-user>
- AUTH_PASSWORD=<your-admin-password>
# Reject any charge point that is not pre-registered in the database.
# This is already the upstream default; pinning it guards against an
# upstream default change.
- AUTO_REGISTER_UNKNOWN_STATIONS=false
ports:
- "8180:8180"
healthcheck:
# 127.0.0.1 and not localhost: Jetty binds IPv4, localhost may resolve
# to ::1 first.
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8180/steve/manager/signin"]
interval: 30s
timeout: 5s
retries: 3
# First boot runs the Flyway migrations against an empty database.
start_period: 150s
steve-db:
image: mariadb:11.8
restart: unless-stopped
environment:
# UTC is mandatory: SteVe requires the database and application time zones
# to be aligned.
- TZ=+00:00
- MARIADB_ROOT_PASSWORD=<your-root-password>
- MARIADB_DATABASE=stevedb
- MARIADB_USER=steve
- MARIADB_PASSWORD=<your-db-password>
volumes:
- ./data/mariadb:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 30s
interval: 10s
timeout: 5s
retries: 5The management UI is then at http://localhost:8180/steve/manager.
No credentials are baked into this image.
SteVe is a Spring Boot application: its application.yml resolves
${db.password}, ${auth.password}, ${db.ip}… from the environment, and
Spring's relaxed binding makes container environment variables win over the
defaults compiled into application-docker.properties.
Those compiled-in defaults — changeme, admin, 1234 — are upstream public
placeholders, not secrets. They are the values anyone gets from a stock
upstream build. Override every one of them that matters to you.
| Variable | Overrides | Default in the image |
|---|---|---|
DB_IP |
database host | mariadb |
DB_PORT |
database port | 3306 |
DB_SCHEMA |
database name | stevedb |
DB_USER |
database user | steve |
DB_PASSWORD |
database password | changeme |
AUTH_USER |
management UI user | admin |
AUTH_PASSWORD |
management UI password | 1234 |
WEBAPI_KEY |
REST WebAPI key | upstream default |
AUTO_REGISTER_UNKNOWN_STATIONS |
accept unknown charge points | false |
See upstream's application.properties for the full list.
The image runs as UID/GID
10001, not root.
The OCPP endpoint and the management UI share port 8180. Charge points connect
over WebSocket without an interactive login, so they cannot pass an
authentication proxy. If you expose the UI publicly behind a reverse proxy,
route only the UI hostname and keep the OCPP endpoint off the public interface —
for example by publishing it on a VPN address only.
The build needs a live MariaDB: SteVe's jOOQ code generation and its Flyway
migrations read a real schema during mvn package. These are exactly the
commands CI runs, so a CI failure reproduces locally:
docker run -d --name steve-build-db \
-p 3306:3306 \
-e MARIADB_ROOT_PASSWORD=root \
-e MARIADB_DATABASE=stevedb \
-e MARIADB_USER=steve \
-e MARIADB_PASSWORD=changeme \
-e TZ=+00:00 \
mariadb:11.8 --innodb-use-native-aio=0
docker build \
--network=host \
--build-arg STEVE_REF=steve-3.14.1 \
--build-arg DB_IP=127.0.0.1 \
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--build-arg VCS_REF="$(git rev-parse HEAD)" \
-t steve:local .
docker rm -f steve-build-dbBUILD_DATE and VCS_REF stamp the org.opencontainers.image.created and
.revision labels; omit them and those two labels come out empty.
--network=host is what lets the RUN steps reach the database on
127.0.0.1. BuildKit only accepts host, none or default for --network,
so a dedicated Docker network is not an option; the legacy builder that allowed
one has been deprecated since Docker Engine 23.
Publishing is the release branch moving to main. From the Actions tab,
run the Release workflow on main — nothing else to fill in. Or, equally,
from a terminal:
git push origin main:releaseEither way the image builds on a GitHub-hosted runner and is pushed to GHCR,
with the resulting digest printed at the end, ready to pin. The version built is
whatever ARG STEVE_REF says in the Dockerfile on that commit — the single
place the release is pinned in code, which is why the workflow asks for no
version.
The workflow adds one check the bare push cannot make: it refuses when the tag
is already published and the packaging has not changed since, because that
release would republish an identical image under a new digest and move the tag
for everyone pinning it. A Temurin bump, which legitimately republishes the same
tag, is not affected. Run it yourself with ./hack/release-preflight.sh.
Merging to main does not publish. "The packaging changed" and "a release
should go out" are different events, and tying them together moved the release
tag whenever a comment did. What merging does is run the same workflow on the
pull request, everything but the push, so a change is proven to build before it
lands. And because shipping still moves a branch, what is waiting to go out
stays a plain git question:
git log release..main -- Dockerfile .dockerignore entrypoint.sh flyway-callbacksA Release drift workflow covers what git cannot answer: whether a push to
release actually produced an image. It compares the
org.opencontainers.image.revision label of the published tag against the
packaging on release, and warns when they diverge — a build that failed after
the branch moved would otherwise leave the branch claiming a release that never
landed.
Separately, every published steve-X.Y.Z tag is re-scanned weekly with
Trivy and the results land in the repository's Security
tab. Scanning on a schedule rather than at build time is deliberate: an image is
clean the day it is built, and what you need to know is whether the tag you
pinned has drifted since.
The packaging files in this repository are Apache-2.0 (see LICENSE).
The image they produce is not: it aggregates SteVe (GPL-3.0-or-later, built from an unmodified upstream tag), the Flyway CLI Open Source Edition (Apache-2.0), and Eclipse Temurin JREs (GPL-2.0 with Classpath Exception).
See NOTICE for the full breakdown and for how the GPLv3 corresponding source requirement is met.