Skip to content

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 compose v2 (multi-arch images: linux/amd64 and linux/arm64 — Raspberry Pi 4/5 supported natively)
  • At least 2 GB RAM, 10 GB disk (InfluxDB data grows over time)
  • Network access to ghcr.io for image pulls and api.github.com for version checks
curl -fsSL https://raw.githubusercontent.com/mchacher/sowel/main/scripts/install.sh | sh

What it does:

  • Checks that Docker and Docker Compose v2 are installed and reachable
  • Creates ~/sowel/ (override with SOWEL_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/health to 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 the sowel-data volume)
  • 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:

cp docker-compose.override.example.yml docker-compose.override.yml
docker compose up -d

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:

docker compose pull && docker compose up -d sowel

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:

curl -s http://localhost:3000/api/v1/health | jq

Restart

docker compose restart sowel

The ring buffer is in-memory, so restart clears it. The file log (data/logs/sowel-N.log) survives.

Stop / start

docker compose stop
docker compose start

Rebuild container (without image change)

docker compose up -d --force-recreate sowel

Updates

Sowel supports two paths: self-update from the UI (easy) and manual update via compose (fallback).

Path 1 — Self-update from UI (preferred)

  1. Sign in as admin
  2. Open the settings / version badge — if an update is available, a badge shows "vX.Y.Z"
  3. Click the badge → confirm in the modal
  4. Sowel creates an automatic backup, then spawns a helper container that does the swap
  5. The UI shows an "Update in progress" overlay
  6. 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.sock mounted in the sowel container
  • docker-compose.yml in 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.yml to ghcr.io/mchacher/sowel:1.0.7 before pull

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:

docker compose restart sowel

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

docker logs --tail 50 sowel

Common causes:

  • Database migration error — check for migration failed in logs
  • Missing /var/run/docker.sock but self-update is enabled — should only warn, not crash
  • InfluxDB not reachable — check sowel-influxdb is up

Integration not connecting

  1. Check status in UI Admin → Integrations or via GET /api/v1/integrations
  2. Check logs for the specific plugin module: plugin:<id>
  3. Check settings are configured under integration.<id>.* in the settings table

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 spawned is the last line before the swap
  • If sowel never came back, check docker ps -a to 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/Paris explicitly set in compose (workaround pending spec 061)
  • Current version: tracked via git log specs/060-self-update-helper-and-detection/ and docker logs sowel | grep "Sowel engine started"
  • Backups: local in data/backups/ (auto), manual downloads on maintainer's Mac
  • MQTT: external mosquitto running on the same VM (not in compose), used by zigbee2mqtt and lora2mqtt plugins
  • 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.