Deployment Guide¶
This guide covers deploying, updating, backing up, and troubleshooting Sowel in production.
Initial deployment¶
Sowel ships as a Docker image at ghcr.io/mchacher/sowel:latest. A production deployment consists of two containers (sowel + sowel-influxdb) orchestrated by docker compose.
Prerequisites¶
- Linux or macOS host with Docker Engine 20.10+ and
docker composev2 (multi-arch images:linux/amd64andlinux/arm64— Raspberry Pi 4/5 supported natively) - At least 2 GB RAM, 10 GB disk (InfluxDB data grows over time)
- Network access to
ghcr.iofor image pulls andapi.github.comfor version checks
Option A — One-command install (recommended)¶
What it does:
- Checks that Docker and Docker Compose v2 are installed and reachable
- Creates
~/sowel/(override withSOWEL_DIR=/opt/sowel) - Downloads the reference
docker-compose.yml - Auto-detects the host timezone and patches the compose file (no more UTC logs)
- Pulls images, starts the stack, waits for
/api/v1/healthto respond - Prints the URL plus the most useful commands (logs / update / stop)
Override the host port: SOWEL_PORT=8080 curl -fsSL ... | sh.
If a previous install is detected at $SOWEL_DIR, the script refuses to overwrite — pick a different directory or remove it first.
Option B — Manual deployment¶
For full control over each step:
# 1. Pick a deployment directory (convention: /opt/sowel)
sudo mkdir -p /opt/sowel
sudo chown $USER:$USER /opt/sowel
cd /opt/sowel
# 2. Download the reference docker-compose.yml
curl -O https://raw.githubusercontent.com/mchacher/sowel/main/docker-compose.yml
# 3. Optional: set your timezone (recommended — fixes calendar scheduling,
# HP/HC tariff classification, sunrise/sunset display)
# Edit docker-compose.yml and uncomment / add:
# - TZ=Europe/Paris
# 4. Launch
docker compose up -d
# 5. Check containers are up
docker compose ps
# 6. Open the UI and create the first admin
open http://<host>:3000
On first boot, Sowel:
- Creates its SQLite DB at
/app/data/sowel.db(on thesowel-datavolume) - Generates a persisted JWT secret (
data/.jwt-secret) and InfluxDB admin token - Waits for you to create the first admin via the UI setup screen
Volumes¶
| Volume | Mount | Content |
|---|---|---|
sowel-data |
/app/data |
SQLite DB, logs, secrets, backups, data files |
sowel-plugins |
/app/plugins |
Installed plugin files (dist/, manifest.json, etc.) |
influxdb-data |
/var/lib/influxdb2 |
Time-series storage |
These are named Docker volumes, persistent across container recreation. They are what make self-update and backup/restore work — the stateful data survives.
Container user (spec 105)¶
Since v1.7.0, the Sowel container runs as a non-root user (sowel, uid 1000). The image's entrypoint (docker-entrypoint.sh) starts briefly as root, runs chown -R sowel:sowel /app/data /app/plugins idempotently, then drops to the sowel user via gosu. This makes upgrades from older root-running versions transparent: docker compose pull && docker compose up -d is enough, no manual chown is required on existing root-owned volumes.
If you override user: in a docker-compose.override.yml to run as a different UID, the entrypoint skips the chown and you must manage volume ownership yourself.
Self-update is opt-in (spec 105)¶
For self-update to work, the Docker socket must be mounted. Since v1.7.0 this is opt-in — the default docker-compose.yml no longer mounts it.
To enable in-app self-update, copy the override template and bring the stack up again:
The override mounts /var/run/docker.sock into the container, restoring the "Update now" button in the UI. Read the security note at the top of the override file before enabling: mounting the Docker socket gives the container effective control over the Docker daemon on the host, so a successful RCE against Sowel would escalate to host root.
Without this override, the "Update now" button is disabled in the UI and POST /api/v1/system/update returns 503. You can still update manually:
Operations¶
Checking status¶
docker compose ps
docker logs -f sowel # live logs from stdout
docker logs --tail 100 sowel # last 100 lines
Or via the API:
Restart¶
The ring buffer is in-memory, so restart clears it. The file log (data/logs/sowel-N.log) survives.
Stop / start¶
Rebuild container (without image change)¶
Updates¶
Sowel supports two paths: self-update from the UI (easy) and manual update via compose (fallback).
Path 1 — Self-update from UI (preferred)¶
- Sign in as admin
- Open the settings / version badge — if an update is available, a badge shows "vX.Y.Z"
- Click the badge → confirm in the modal
- Sowel creates an automatic backup, then spawns a helper container that does the swap
- The UI shows an "Update in progress" overlay
- After ~30-90 seconds, the page reloads on the new version
Requirements:
- Running under
docker compose(Sowel detects this via container labels) /var/run/docker.sockmounted in the sowel containerdocker-compose.ymlin a bind-mounted or accessible directory on the host
If any requirement is missing, the Update button is disabled with a tooltip explaining what to do.
Path 2 — Manual update via docker compose (fallback)¶
cd /opt/sowel
docker compose pull sowel # fetch the latest image from ghcr.io
docker compose up -d sowel # recreate the container
Sowel restarts, migrations run automatically, plugins are auto-downloaded if missing (spec 058), and the UI resumes.
Use this when:
- Self-update UI is disabled (no docker socket, non-compose deployment)
- Upgrading across a version that itself contains a self-update bug (e.g. from v1.0.6, which had the race condition fixed in v1.0.7)
- You want to pin a specific version — edit
docker-compose.ymltoghcr.io/mchacher/sowel:1.0.7beforepull
Backup & Restore¶
Backups capture SQLite, InfluxDB data, and all dynamic data/* files into a single ZIP.
Manual backup (export)¶
From the UI: Admin → Backup → "Download a backup". The browser downloads a sowel-backup-<date>.zip file.
From the API:
TOKEN=$(curl -s http://<host>:3000/api/v1/auth/login \
-H "Content-Type: application/json" \
--data-raw '{"username":"admin","password":"<pwd>"}' | jq -r .accessToken)
curl -s http://<host>:3000/api/v1/backup \
-H "Authorization: Bearer $TOKEN" \
-o sowel-backup.zip
Automatic pre-update backups (local)¶
Before every self-update, Sowel creates a backup in data/backups/sowel-backup-pre-v<version>-<timestamp>.zip. The three most recent are kept; older are rotated out.
Listing local backups:
- From the UI: Admin → Backup → "Local backups" section
- Via the API:
GET /api/v1/backup/local
Restoring a local backup:
- From the UI: click "Restore" next to the backup in the list
- Via the API:
POST /api/v1/backup/restore-local { "filename": "sowel-backup-pre-v1.0.7-2026-04-11T08-28-45.zip" }
Manual restore (import)¶
From the UI: Admin → Backup → "Upload a backup".
From the API:
curl -s -X POST http://<host>:3000/api/v1/backup \
-H "Authorization: Bearer $TOKEN" \
-F "file=@sowel-backup.zip"
After restore, Sowel reports { restartRequired: true }. You must restart the container for the restored state to take full effect:
Archive contents¶
See the "Backup & Restore" section in architecture.md for the full format.
Logging access¶
Three sources¶
| Source | Retention | Use case |
|---|---|---|
| Ring buffer (memory) | Lost on restart | Live tail via UI Admin → Logs |
| Docker stdout | Per-container (lost on recreate) | docker logs sowel |
pino-roll files on sowel-data volume |
14 daily files, survives recreation | Post-incident investigation — the only source that survives self-update recreates |
Accessing the file logs¶
# List files
docker exec sowel ls -la /app/data/logs/
# View today's log
docker exec sowel cat /app/data/logs/sowel.6.log
# Grep errors/warns in a time window
docker exec sowel sh -c 'cat /app/data/logs/sowel.6.log | grep -E "2026-04-11T07:" | grep -E "\"level\":\"(error|warn)\""'
Via the helper script¶
From the repo (on your dev machine):
SOWEL_URL=http://<host>:3000 SOWEL_PASSWORD='<pwd>' \
python3 scripts/logs/fetch-logs.py "" error 100
# Filter by module
SOWEL_URL=http://<host>:3000 SOWEL_PASSWORD='<pwd>' \
python3 scripts/logs/fetch-logs.py recipe-manager debug 50
This queries the ring buffer via the API — so only logs since the last restart.
Temporarily raising the log level¶
curl -s -X PUT http://<host>:3000/api/v1/logs/level \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"level":"debug"}'
This affects the ring buffer only (always at debug by default) — the file transport is at the root level set via LOG_LEVEL env var.
Version check¶
TOKEN=$(curl -s -X POST http://<host>:3000/api/v1/auth/login \
-H "Content-Type: application/json" \
--data-raw '{"username":"admin","password":"<pwd>"}' | jq -r .accessToken)
curl -s http://<host>:3000/api/v1/system/version \
-H "Authorization: Bearer $TOKEN" | jq
Expected response:
{
"current": "1.0.8",
"latest": "1.0.8",
"updateAvailable": false,
"releaseUrl": "https://github.com/mchacher/sowel/releases/tag/v1.0.8",
"dockerAvailable": true,
"composeManaged": true
}
Force a fresh GitHub poll:
curl -s -X POST http://<host>:3000/api/v1/system/version/check \
-H "Authorization: Bearer $TOKEN" | jq
Troubleshooting¶
Container keeps restarting¶
Common causes:
- Database migration error — check for
migration failedin logs - Missing
/var/run/docker.sockbut self-update is enabled — should only warn, not crash - InfluxDB not reachable — check
sowel-influxdbis up
Integration not connecting¶
- Check status in UI Admin → Integrations or via
GET /api/v1/integrations - Check logs for the specific plugin module:
plugin:<id> - Check settings are configured under
integration.<id>.*in thesettingstable
Self-update fails¶
Symptoms: click "Update", overlay shows, but page never reloads. Container still on old version.
Recovery:
cd /opt/sowel
docker compose up -d # recreates the current container if helper failed mid-way
# Or manual upgrade:
docker compose pull && docker compose up -d
Investigation:
- Helper container logs are lost if
AutoRemove: true(current default, spec 060) - Check sowel's own logs right before the helper was spawned:
Update helper spawnedis the last line before the swap - If sowel never came back, check
docker ps -ato see if the container is Exited
Database corrupted¶
SQLite is WAL mode — safe for abrupt shutdowns in most cases. If corruption:
# Stop sowel
docker compose stop sowel
# Backup the corrupted DB
docker run --rm -v /opt/sowel_sowel-data:/data alpine cp /data/sowel.db /data/sowel.db.broken
# Restore from the most recent local backup
docker run --rm -v /opt/sowel_sowel-data:/data alpine ls /data/backups/
# Then use the restore flow (see above)
InfluxDB bucket missing after restore¶
If you restore to a fresh machine, InfluxDB may not have buckets yet. The current restore flow (spec 059) calls ensureBuckets() and ensureEnergyBuckets() before writing data, so this should be automatic. If not, check sowel-influxdb logs.
Time-based logic broken (shutters at wrong time, HP/HC wrong)¶
The container defaults to UTC. Set TZ=Europe/Paris (or your timezone) in docker-compose.yml → restart. See architecture.md § Timezone handling and spec 061 at github.com/mchacher/sowel/tree/main/specs/061-timezone-from-home-location.
Production reference — current deployment¶
The maintainer's production deployment (as of 2026-04-11):
- Host: Proxmox VM
sowelox(Linux, x86_64, 8 GB RAM) - Path:
/opt/sowel/ - Access: LAN
http://192.168.0.230:3000 - Containers:
sowel+sowel-influxdb - Timezone:
TZ=Europe/Parisexplicitly set in compose (workaround pending spec 061) - Current version: tracked via
git log specs/060-self-update-helper-and-detection/anddocker logs sowel | grep "Sowel engine started" - Backups: local in
data/backups/(auto), manual downloads on maintainer's Mac - MQTT: external
mosquittorunning on the same VM (not in compose), used byzigbee2mqttandlora2mqttplugins - Zigbee2MQTT: external daemon on sowelox, not managed by Sowel itself
The connectivity graph:
Internet
|
Cloudflare Tunnel
|
sowelox (Linux VM)
+-- docker: sowel (port 3000)
+-- docker: sowel-influxdb
+-- docker: mosquitto (MQTT broker, 1883)
+-- systemd: zigbee2mqtt (reads Zigbee coordinator USB)
+-- systemd: lora2mqtt (reads LoRa dongle USB)
+-- systemd: cloudflared (tunnel)
See the memory file reference_sowel_access.md for SSH / API credentials.