HELIOS accesses Docker via the Unix socket /var/run/docker.sock.
Why Docker Socket?
- Industry standard (used by Portainer, Watchtower, etc.)
- Full Docker API access
- No additional configuration required
- Works with Docker Compose operations
Required permissions:
- Socket must be mounted into HELIOS container
- HELIOS runs with access to docker group (or root)
HELIOS needs read/write access to:
compose.yaml– to add/modify services.env– to manage environment variables
Solution: Mount the stack directory and the Docker socket into the HELIOS container (see helios.rb).
volumes:
- .:/data # Compose resolves this relative to compose.yaml
- /var/run/docker.sock:/var/run/docker.sockInside the container, /data holds compose.yaml, .env, and a helios/ subdirectory for config.yaml and HELIOS' own SQLite files.
HELIOS performs these operations via Docker API / CLI:
| Operation | How |
|---|---|
| Read running containers | Docker API: GET /containers/json |
| Start stack | docker compose up --no-build -d <services-except-helios> |
| Stop stack | docker compose down <services-except-helios> |
| Recreate service | docker compose pull <svc> → down <svc> → up --no-build -d |
| Pull new images | docker compose pull |
| View logs | docker compose logs (streamed) + Docker API for recent lines |
| Listen for events | Docker API: GET /events (streaming) |
HELIOS uses a hybrid approach:
1. docker-api Gem – for direct Docker API access
# Gemfile
gem 'docker-api'
# Usage
Docker.url = 'unix:///var/run/docker.sock'
containers = Docker::Container.all
container.logs(stdout: true, tail: 100)
container.json['State']['Health']['Status']Used for:
- Reading container status and health
- Streaming logs
- Inspecting container details
2. CLI via Open3 – for Docker Compose operations (see Orchestration::Runner)
require 'open3'
def run_compose(*args)
cmd = [
'docker', 'compose',
'-f', ::Compose.path,
'--project-directory', host_data_path,
'--env-file', ::Env.path,
'--progress', 'plain',
*args,
]
output, status = Open3.capture2e(*cmd)
raise CommandError, output unless status.success?
output
endUsed for:
up --no-build -d– start stack (all services exceptheliositself)down– stop stackpull– pull new imagespull <svc>→down <svc>→up --no-build -d <svc>– recreate a servicelogs -f --timestamps– live log streaming viaIO.popenconfig --hash '*'– detect services whose effective config has drifted
3. Events listener – streams GET /events from the Docker API in a background thread (see Orchestration::EventsListener and events_listener/streaming.rb) and broadcasts service status changes to the browser via Turbo Streams. Only active while at least one subscriber is connected.
Rationale: The docker-api gem provides clean Ruby access to container info and logs, but cannot execute docker compose commands — those require the CLI because they involve YAML parsing, service dependencies, and network setup that only Compose handles. The events listener complements both: it turns Docker's push-based event stream into live UI updates instead of per-request polling.
HELIOS generates two files in the stack directory on every configuration change (and before every compose operation). Both are fully owned by HELIOS — except for parts explicitly tracked as "unmanaged" (user-added services / env vars).
Generated by Export::Builder, which iterates over all Export::Services::* classes and includes each whose enabled?(configuration) predicate returns true.
Service definitions (see app/services/export/services/):
| Service | Always / conditional |
|---|---|
helios |
Always (except in development, where HELIOS runs natively) |
dashboard |
Always |
power_splitter |
When grid_import_power, house_power and ≥1 further consumer are mapped |
forecast_collector |
Always |
postgresql |
Always |
redis |
Always |
influxdb |
Always |
watchtower |
Always |
senec_collector |
When a sensor is mapped to a SENEC source |
shelly_collector |
One instance per configured Shelly device |
mqtt_collector |
When a sensor is mapped to an MQTT source |
ingest |
When external push source (ioBroker / Home Assistant) is used |
traefik |
When HTTPS / reverse proxy is enabled |
Images come from a mix of hard-coded defaults and user-configurable values (configuration.<service>.image in config.yaml) — see each service class for details. Versioning follows the strategy in Image Versioning Strategy below.
Each service declares its own healthcheck, and dependent services use depends_on: service_healthy. Unmanaged services (imported from existing installations) are appended verbatim.
Generated by Export::Env from the config.yaml singletons, on top of the low-level Env::File parser. Comments and unknown variables from an existing .env are preserved on round-trip (see ADR-0008).
Variables include timezone, installation date, admin password, all secrets (DB passwords, InfluxDB token, SECRET_KEY_BASE), INFLUX_SENSOR_* mappings, and per-service connection settings. Secrets are auto-generated via SecureRandom on first setup and persisted in config.yaml.
HELIOS detects the installation scenario at startup (see product.md for details).
Detection method: Import::StackReader parses the existing compose.yaml / .env; Import::ConfigurationImporter classifies what it finds. If the stack contains no services other than HELIOS, the setup wizard runs. If a local target (dashboard / influxdb) or any known collector service is present, an import pass pre-fills config.yaml. A collectors-only install (collectors present, but no local dashboard/InfluxDB) is detected via ConfigurationImporter#collectors_only?.
Scenario A/B (Fresh install): Only HELIOS service exists → setup wizard. User configures devices and selects a data source per device (SENEC/Shelly/MQTT or ioBroker/HA). Collector services are generated only for devices with direct hardware data sources. The distinction between standalone and smart home setups is implicit — no separate wizard question.
Scenario C (Existing installation): Other services present → HELIOS automatically imports compose.yaml and .env on first access, reverse-maps configuration into internal singletons (best-effort), and shows the result to the user for review.
If Docker socket is not accessible (not mounted, daemon stopped):
- HELIOS shows a dedicated error page
- Message: "Cannot connect to Docker"
- Troubleshooting hints provided (check socket mount, Docker daemon status)
- No other functionality available until resolved
If compose.yaml is missing or invalid after initial setup:
- HELIOS can regenerate the file from
config.yaml(see ADR-0009) - User is prompted: "Configuration file missing. Regenerate from saved state?"
- Regeneration restores all HELIOS-managed services
- User-added services cannot be recovered (warning shown)
HELIOS identifies containers in its stack via Docker Compose labels. Compose sets these on every container it manages:
com.docker.compose.project– project namecom.docker.compose.service– service name from compose.yamlcom.docker.compose.config-hash– hash of the effective service config (used to detect drift)
Fixed project name. Instead of deriving the project name at runtime, HELIOS requires a fixed value:
# app/services/orchestration.rb
PROJECT_NAME = 'solectrus'.freezeOn startup, StartupCheck#check_compose_project_name parses the top-level name: in compose.yaml and refuses to proceed unless it equals solectrus. This guarantees that container lookups by label always hit the right project, regardless of the host directory name.
Containers are then enumerated via the Docker API and filtered by com.docker.compose.project=solectrus (see Orchestration::Container.all).
Defaults live in ConfigSchema; per-service images can be overridden via config.yaml.
| Service | Image Tag | Rationale |
|---|---|---|
| SOLECTRUS Dashboard | ghcr.io/solectrus/solectrus:latest |
Own service, always latest |
| HELIOS | ghcr.io/solectrus/helios:develop |
Own service, currently pre-release |
| Power-Splitter | ghcr.io/solectrus/power-splitter:latest |
Own service, always latest |
| Forecast-Collector | ghcr.io/solectrus/forecast-collector:latest |
Own service, always latest |
| SENEC-Collector | ghcr.io/solectrus/senec-collector:latest |
Own service, always latest |
| MQTT-Collector | ghcr.io/solectrus/mqtt-collector:latest |
Own service, always latest |
| Shelly-Collector | ghcr.io/solectrus/shelly-collector:latest |
Own service, always latest |
| Ingest | ghcr.io/solectrus/ingest:latest |
Own service, always latest |
| PostgreSQL | postgres:18-alpine |
Major version pinned |
| Redis | redis:8-alpine |
Major version pinned |
| InfluxDB | influxdb:2.9-alpine |
Minor version pinned |
| Traefik | traefik:v3.7 |
Minor version pinned |
| Watchtower | nickfedor/watchtower:latest |
Fork with additional features |
Rationale:
- Own services use
latest– Watchtower handles updates automatically - Third-party services pin major version – prevents breaking changes, allows minor/patch updates
heliosstill tracksdevelopuntil its first stable release
When HELIOS needs to modify compose.yaml (e.g., adding a service), it may encounter user modifications.
Strategy: Detect and warn
- HELIOS tracks which services it manages (stored in
config.yaml) - Before modifying, compare current file with expected state
- If differences detected in managed services → show warning to user
- User decides: apply changes, skip, or review diff
What HELIOS tracks:
- Services it created (e.g.,
dashboard,postgresql,influxdb) - Expected configuration for each service
What HELIOS preserves:
- Services it doesn't manage (user-added, e.g.,
traefik,dozzle) are stored as "unmanaged" inConfiguration#data - Unmanaged services are written back to
compose.yamlverbatim (with${VAR}references intact) - Unknown
.envvariables referenced by unmanaged services are preserved in a dedicated section - A future web-based editor will allow power-users to modify unmanaged services directly in HELIOS
Example conflict scenarios:
| Scenario | HELIOS behavior |
|---|---|
User changed port of dashboard |
Warning: "Port was modified. Keep your change or reset?" |
User added traefik service |
No warning, service is preserved as unmanaged |
User removed redis |
Warning: "Required service missing. Re-add?" |
Health checks are defined natively in Docker Compose. HELIOS reads the health status from Docker API.
Approach:
- Each service defines its own
healthcheckin compose.yaml - Docker reports status:
healthy,unhealthy,starting - HELIOS queries:
docker inspect --format '{{.State.Health.Status}}' <container>
Example health checks for compose.yaml:
postgresql:
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
redis:
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 5s
retries: 5
influxdb:
healthcheck:
test: ['CMD', 'influx', 'ping']
interval: 10s
timeout: 5s
retries: 5
dashboard:
healthcheck:
test: ['CMD-SHELL', 'nc -z 127.0.0.1 3000 || exit 1']
interval: 10s
timeout: 5s
retries: 3HELIOS behavior:
- Displays overall status: "All services healthy" or "Problem detected"
- On problem: Shows which service is unhealthy
- Status is pushed to the browser live via
Orchestration::EventsListener, which streams Docker'sGET /eventsendpoint and broadcasts via Turbo Streams — no per-request polling in the UI Orchestration::StackStatustightens the refresh cadence while services are in a transient:startingstate, to catch fast health transitions the event stream may coalesce