Skip to content
martin edited this page Aug 16, 2026 · 6 revisions

Docker

Advanced — Get started does not need any of this. Reach for a container when you want the controller running unattended on a machine you do not develop on: a Raspberry Pi, a home server, a NAS. The payoff is that you never build on the target machine — CI publishes every platform to GHCR and the target only pulls, which matters on a Pi where a local .NET build is slow enough to be impractical.

Everything below assumes Docker Engine with the Compose v2 plugin, and the same reachability check Get started opens with:

docker --version
docker compose version

nc -vz 192.168.2.10 502   # inverter
nc -vz 192.168.2.6  502   # EV charger

1. Pick your image tag

One name covers every platform. ghcr.io/mpospisil/solax-controller is a multi-platform manifest list, so the same tag resolves to the correct image on a Raspberry Pi, an x64 Linux server or a Windows host — you name a version, never an architecture.

Tag What it is Use it for
:latest Manifest list, built from main Tracking development
:1.0.0 :1.0 :1 Manifest list, from a v* release tag Normal production use
:sha-abc1234 Manifest list, every build Rollback to a known-good build
:<version>-linux-arm64 Single platform Pinning one architecture
:<version>-linux-amd64 Single platform Pinning one architecture
:<version>-nanoserver-ltsc2022 Single platform Pinning the Windows image

Prefer a manifest-list tag. Reach for a platform-suffixed one only when you deliberately want to stop a host resolving its own architecture.

2. Start a single container

The quickest useful thing: the controller polling and logging, serving its own web UI, and writing nothing to your hardware. Create the host directories first and hand them to the image's non-root user, uid 1654 — the container cannot write to them otherwise.

mkdir -p /opt/solax/{data,logs}
sudo chown -R 1654:1654 /opt/solax/data /opt/solax/logs

docker run -d --name solax-controller --restart on-failure \
  -e TZ=Europe/Prague \
  -e Solax__Inverter__Host=192.168.2.10 \
  -e Solax__EvCharger__Host=192.168.2.6 \
  -p 8090:8090 \
  -v /opt/solax/data:/app/data \
  -v /opt/solax/logs:/app/logs \
  ghcr.io/mpospisil/solax-controller:latest

The two inverter addresses are the only settings here that have to be yours. Open http://localhost:8090 — or the host's LAN address — and the dashboard, controls, session history and forecast plan are all there, with no login. Nothing enabled it: the UI is on by default, and -p 8090:8090 is only what makes the container's port reachable from outside it.

Every setting in appsettings.json can be overridden this way, : written as __ — see Configuration. On a Raspberry Pi or a Windows host there is one extra thing to get right first: Platforms.

Putting a password on the web UI (optional)

The UI above has no login — anyone who can reach the port gets the dashboard and every control on it, including the ones that write to the inverter and charger. On a household LAN that is usually fine, and it is what lets the container above work with nothing configured. Add a password if the network has guests on it, or if the port is reachable from beyond it.

It is one environment variable. Generate the hash first — no config, no listening socket, it prints and exits — then pass the hash, never the password:

docker run --rm ghcr.io/mpospisil/solax-controller:latest hash-password '<your password>'

docker run -d --name solax-controller --restart on-failure \
  -e TZ=Europe/Prague \
  -e Solax__Inverter__Host=192.168.2.10 \
  -e Solax__EvCharger__Host=192.168.2.6 \
  -e Web__PasswordHash='<the hash>' \
  -p 8090:8090 \
  -v /opt/solax/data:/app/data \
  -v /opt/solax/logs:/app/logs \
  ghcr.io/mpospisil/solax-controller:latest

Every page now redirects to a login form. There is no second switch: the hash's presence is what requires the login, so a configured password can't end up unenforced. Web__RequireAuthentication can override that in either direction and is rarely worth setting — see Configuration for the full table.

Running without the UI

-e Web__Enabled=false makes the controller headless again: no socket is bound inside the container at all, and -p 8090:8090 would then publish a port whose connections are refused. Drop both if you want the MQTT-only deployment.

3. Verify it is working

The first line of the log is the running build — for example 1.0.0 (31bf347). A version of 0.0.0-dev with no commit hash means a local build rather than anything CI published.

docker logs -f solax-controller

# memory limits are actually being applied (Linux)
docker stats --no-stream

Leave the charge mode on Off until the telemetry looks right — that is the recommended way to confirm you are reading the correct registers before anything writes. The log says which state the UI is in: Web UI enabled; listening on port 8090, login not required. With Web__Enabled=false there is no such line, nothing on 8090 and no socket in ss -ltnp; that is the expected state, not a fault.

4. Stopping it

Ask the service to stop rather than killing it. The Health page in the web UI has a Stop service button (Home Assistant gets one too), and docker stop solax-controller does the same thing. All three run the same graceful shutdown: the charger's current setpoint is returned to the pause current, the open charging session is closed and written, the session store is flushed.

docker kill, docker rm -f and kill -9 skip all of it. Little is lost from disk — SQLite is crash-safe here and the log is written unbuffered — but nothing revokes the charging current, so a car keeps drawing at whatever the controller last wrote until something else changes it.

--restart on-failure above is what makes a stop stay a stop. The controller sets its exit code deliberately so the policy can tell the two cases apart:

How the run ended Exit code What Docker does
Stop service in the UI or Home Assistant 0 leaves it down until you start it
docker stop — leaves it down (a manual stop always wins)
Reboot, daemon restart, docker restart 143 starts it again
Crash, OOM kill, power cut non-zero starts it again

With --restart unless-stopped instead, the Stop button becomes a restart button — the container comes back a second later. Start it again with docker start solax-controller.

Every run that ends properly says so on its last line:

SolaX Local Controller stopped cleanly at the request of Web UI. Exiting with code 0: …
SolaX Local Controller stopped cleanly after a termination signal. Exiting with code 143: …

A log that ends without one of those is a run that died.

5. Move up to Compose

A single docker run is fine for a first look, but the repository ships a Compose deployment that keeps configuration in one .env file on the target and can start Home Assistant and an MQTT broker alongside the controller. That — and the deploy.sh / deploy-controller-only.sh split that decides which containers actually run — is on the Deployment page.


Previous: Configuration · Next: Platforms

SolaX Local Controller

Advanced


Images: ghcr.io/mpospisil/solax-controller

Clone this wiki locally