feat(domoticz): Rewrite from Docker to LXC Debian container

- Switch from Docker to LXC with Debian bookworm rootfs and native
  Domoticz binary from GitHub releases (latest/download pattern)
- Fix LXC cgroup2 terminal allocation: add lxc.tty.max, lxc.pty.max,
  cgroup2 device permissions for standard char devices, disable seccomp
- Fix PID 1 issue: run domoticz as child process with signal forwarding
- Use quoted heredoc with sed placeholders for start script generation
- Update LuCI view: Docker → LXC references, add memory usage display
- Remove Docker image UCI option, update catalog runtime to "lxc"
- Fix streamlit LXC config: same cgroup2/terminal/seccomp fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-04 22:05:39 +01:00
parent 89896568b1
commit f2f24afe12
11 changed files with 566 additions and 140 deletions

View File

@ -136,4 +136,5 @@ _Last updated: 2026-02-04_
- `zigbee2mqtt`: Direct `/dev/ttyUSB0` passthrough (socat TCP bridge fails ASH protocol), adapter `ezsp`→`ember` (z2m 2.x), `ZIGBEE2MQTT_DATA` env var, `mosquitto-nossl` dependency.
- `smbfs`: New SMB/CIFS remote mount manager package — UCI config, `smbfsctl` CLI (add/remove/mount/umount/test/status), auto-mount init script, credentials storage, Jellyfin+Lyrion integration, catalog entry.
- `jellyfin`: KISS READMEs for both backend and LuCI packages.
- `domoticz`: New `luci-app-domoticz` LuCI dashboard with IoT integration status (Mosquitto, Zigbee2MQTT, MQTT bridge), service lifecycle, HAProxy, mesh P2P, logs. `domoticzctl` enhanced with `configure-mqtt` (auto Mosquitto+Z2M bridge), `configure-haproxy`, `backup/restore`, `mesh-register`, `uninstall`. UCI extended with mqtt/network/mesh sections. Catalog updated.
- `domoticz`: Rewrite from Docker to LXC Debian container with native binary from GitHub releases. LuCI dashboard with IoT integration status (Mosquitto, Zigbee2MQTT, MQTT bridge), service lifecycle, HAProxy, mesh P2P, logs. `domoticzctl` with `configure-mqtt` (auto Mosquitto+Z2M bridge), `configure-haproxy`, `backup/restore`, `mesh-register`, `uninstall`. UCI extended with mqtt/network/mesh sections. Catalog updated.
- LXC cgroup2 fix: Added `lxc.tty.max`, `lxc.pty.max`, `lxc.cgroup2.devices.allow` for standard character devices, and `lxc.seccomp.profile` disable to fix terminal allocation failures on cgroup v2 systems. Applied to `streamlit` and `domoticz`.

View File

@ -270,7 +270,8 @@
"Bash(/home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/local-build.sh:*)",
"Bash(do rsync -av --delete /home/reepost/CyberMindStudio/secubox-openwrt/package/secubox/$pkg/ /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/local-feed/$pkg/)",
"Bash(do rsync -av --delete \"package/secubox/$pkg/\" \"secubox-tools/local-feed/$pkg/\")",
"Bash(xdg-open:*)"
"Bash(xdg-open:*)",
"WebFetch(domain:releases.domoticz.com)"
]
}
}

View File

@ -12,7 +12,7 @@ Requires `secubox-app-domoticz` (installed as dependency).
## Features
- **Service Status**: Container status, Docker availability, disk usage, USB devices
- **Service Status**: Container status, LXC availability, memory/disk usage, USB devices
- **IoT Integration**: Mosquitto broker status, Zigbee2MQTT status, MQTT bridge configuration
- **MQTT Auto-Setup**: One-click Mosquitto installation and broker configuration
- **Network**: HAProxy reverse proxy integration, WAN access control, domain configuration
@ -28,9 +28,9 @@ Requires `secubox-app-domoticz` (installed as dependency).
| `start` | — | Start Domoticz service |
| `stop` | — | Stop Domoticz service |
| `restart` | — | Restart Domoticz service |
| `install` | — | Pull Docker image and configure |
| `install` | — | Create LXC container and download Domoticz |
| `uninstall` | — | Remove container (preserves data) |
| `update` | — | Pull latest image and restart |
| `update` | — | Download latest Domoticz and restart |
| `configure_mqtt` | — | Auto-configure Mosquitto and MQTT bridge |
| `configure_haproxy` | — | Register HAProxy vhost |
| `backup` | — | Create data backup |

View File

@ -119,10 +119,10 @@ return view.extend({
return html;
};
o = s.option(form.DummyValue, '_docker', _('Docker'));
o = s.option(form.DummyValue, '_lxc', _('LXC'));
o.rawhtml = true;
o.cfgvalue = function() {
return status.docker_available
return status.lxc_available
? '<span style="color:#27ae60;">Available</span>'
: '<span style="color:#e74c3c;">Not available</span>';
};
@ -132,10 +132,11 @@ return view.extend({
o.cfgvalue = function() {
var port = status.port || 8080;
var html = '<table style="border-collapse:collapse;">';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Image:</td><td>' + (status.image || '-') + '</td></tr>';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Port:</td><td>' + port + '</td></tr>';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Data:</td><td>' + (status.data_path || '-') + '</td></tr>';
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Domain:</td><td>' + (status.domain || '-') + '</td></tr>';
if (status.memory_usage)
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Memory:</td><td>' + status.memory_usage + '</td></tr>';
if (status.disk_usage)
html += '<tr><td style="padding:2px 12px 2px 0;color:#8892b0;">Disk:</td><td>' + status.disk_usage + '</td></tr>';
if (status.usb_devices && status.usb_devices.length > 0)
@ -210,7 +211,7 @@ return view.extend({
o.inputstyle = 'apply';
o.onclick = function() {
ui.showModal(_('Installing...'), [
E('p', { 'class': 'spinning' }, _('Pulling Docker image and configuring...'))
E('p', { 'class': 'spinning' }, _('Creating LXC container and downloading Domoticz...'))
]);
return callInstall().then(function(res) {
ui.hideModal();
@ -263,11 +264,11 @@ return view.extend({
}
o = s.option(form.Button, '_update', _('Update'));
o.inputtitle = _('Pull Latest Image');
o.inputtitle = _('Update Domoticz');
o.inputstyle = 'action';
o.onclick = function() {
ui.showModal(_('Updating...'), [
E('p', { 'class': 'spinning' }, _('Pulling latest Docker image and restarting...'))
E('p', { 'class': 'spinning' }, _('Downloading latest Domoticz and restarting container...'))
]);
return callUpdate().then(function(res) {
ui.hideModal();
@ -327,10 +328,6 @@ return view.extend({
o.datatype = 'port';
o.placeholder = '8080';
o = s.option(form.Value, 'image', _('Docker Image'),
_('Docker image to use.'));
o.placeholder = 'domoticz/domoticz:latest';
o = s.option(form.Value, 'data_path', _('Data Path'),
_('Path for Domoticz config and database.'));
o.placeholder = '/srv/domoticz';

View File

@ -3,9 +3,20 @@
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CONTAINER="secbx-domoticz"
LXC_NAME="domoticz"
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONF="$LXC_PATH/$LXC_NAME/config"
CONFIG="domoticz"
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONF" ] && [ -d "$LXC_ROOTFS" ]
}
case "$1" in
list)
echo '{"status":{},"start":{},"stop":{},"restart":{},"install":{},"uninstall":{},"update":{},"configure_mqtt":{},"configure_haproxy":{},"backup":{},"restore":{"path":"str"},"logs":{"lines":"int"}}'
@ -16,7 +27,6 @@ case "$1" in
json_init
enabled=$(uci -q get ${CONFIG}.main.enabled)
image=$(uci -q get ${CONFIG}.main.image)
port=$(uci -q get ${CONFIG}.main.port)
data_path=$(uci -q get ${CONFIG}.main.data_path)
devices_path=$(uci -q get ${CONFIG}.main.devices_path)
@ -38,7 +48,6 @@ case "$1" in
mesh_enabled=$(uci -q get ${CONFIG}.mesh.enabled)
json_add_boolean "enabled" ${enabled:-0}
json_add_string "image" "${image:-domoticz/domoticz:latest}"
json_add_int "port" ${port:-8080}
json_add_string "data_path" "${data_path:-/srv/domoticz}"
json_add_string "devices_path" "${devices_path:-/srv/devices}"
@ -55,24 +64,36 @@ case "$1" in
json_add_boolean "firewall_wan" ${firewall_wan:-0}
json_add_boolean "mesh_enabled" ${mesh_enabled:-0}
# Docker availability
if command -v docker >/dev/null 2>&1; then
json_add_boolean "docker_available" 1
# LXC availability
if command -v lxc-start >/dev/null 2>&1; then
json_add_boolean "lxc_available" 1
else
json_add_boolean "docker_available" 0
json_add_boolean "lxc_available" 0
fi
# Container status
if docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null | grep -q .; then
if lxc_running; then
json_add_string "container_status" "running"
uptime=$(docker ps --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null)
uptime=$(lxc-info -n "$LXC_NAME" -s 2>/dev/null | head -1)
json_add_string "container_uptime" "$uptime"
elif docker ps -a --filter "name=$CONTAINER" --format '{{.Status}}' 2>/dev/null | grep -q .; then
# Memory from cgroup
mem_usage=""
if [ -f "/sys/fs/cgroup/lxc.payload.$LXC_NAME/memory.current" ]; then
mem_bytes=$(cat "/sys/fs/cgroup/lxc.payload.$LXC_NAME/memory.current" 2>/dev/null)
if [ -n "$mem_bytes" ] && [ "$mem_bytes" -gt 0 ] 2>/dev/null; then
mem_mb=$(( mem_bytes / 1048576 ))
mem_usage="${mem_mb} MB"
fi
fi
json_add_string "memory_usage" "$mem_usage"
elif lxc_exists; then
json_add_string "container_status" "stopped"
json_add_string "container_uptime" ""
json_add_string "memory_usage" ""
else
json_add_string "container_status" "not_installed"
json_add_string "container_uptime" ""
json_add_string "memory_usage" ""
fi
# Mosquitto broker status
@ -225,7 +246,7 @@ case "$1" in
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
[ -z "$lines" ] && lines=50
logs=$(docker logs --tail "$lines" "$CONTAINER" 2>&1 | tail -100)
logs=$(/usr/sbin/domoticzctl logs "$lines" 2>&1)
json_init
json_add_string "logs" "$logs"
json_dump

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-domoticz
PKG_RELEASE:=3
PKG_RELEASE:=4
PKG_VERSION:=1.0.0
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
@ -14,13 +14,13 @@ define Package/secubox-app-domoticz
CATEGORY:=Utilities
PKGARCH:=all
SUBMENU:=SecuBox Apps
TITLE:=SecuBox Domoticz docker app
DEPENDS:=dockerd +docker +containerd
TITLE:=SecuBox Domoticz LXC app
DEPENDS:=+lxc +lxc-common
endef
define Package/secubox-app-domoticz/description
Installer, configuration, and service manager for running Domoticz
inside Docker on SecuBox-powered OpenWrt systems.
inside an LXC Alpine container on SecuBox-powered OpenWrt systems.
endef
define Package/secubox-app-domoticz/conffiles

View File

@ -1,6 +1,6 @@
# SecuBox Domoticz
Home automation platform running in Docker with MQTT bridge, Zigbee2MQTT integration, and P2P mesh support.
Home automation platform running in an LXC Alpine container with MQTT bridge, Zigbee2MQTT integration, and P2P mesh support.
## Installation
@ -17,7 +17,6 @@ UCI config file: `/etc/config/domoticz`
```
config domoticz 'main'
option enabled '0'
option image 'domoticz/domoticz:latest'
option data_path '/srv/domoticz'
option devices_path '/srv/devices'
option port '8080'
@ -42,9 +41,9 @@ config domoticz 'mesh'
## Usage
```sh
domoticzctl install # Pull image, install prerequisites
domoticzctl install # Create LXC container, download Domoticz
domoticzctl uninstall # Remove container (data preserved)
domoticzctl update # Pull latest image, restart
domoticzctl update # Download latest Domoticz, restart
domoticzctl status # Show container status
domoticzctl logs [-f] # Container logs
domoticzctl configure-mqtt # Auto-setup Mosquitto + MQTT bridge
@ -79,7 +78,7 @@ When `secubox-app-zigbee2mqtt` is installed:
## Dependencies
- `dockerd`, `docker`, `containerd`
- `lxc`, `lxc-common`
- Optional: `mosquitto-nossl`, `secubox-app-zigbee2mqtt`
## License

View File

@ -1,6 +1,5 @@
config domoticz 'main'
option enabled '0'
option image 'domoticz/domoticz:latest'
option data_path '/srv/domoticz'
option devices_path '/srv/devices'
option port '8080'

View File

@ -1,8 +1,13 @@
#!/bin/sh
# SecuBox Domoticz manager — IoT home automation with MQTT/Zigbee integration
# SecuBox Domoticz manager — LXC Alpine container with MQTT/Zigbee integration
CONFIG="domoticz"
CONTAINER="secbx-domoticz"
LXC_NAME="domoticz"
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONF="$LXC_PATH/$LXC_NAME/config"
DATA_PATH_DEFAULT="/srv/domoticz"
LOG_FILE="/srv/domoticz/domoticz.log"
OPKG_UPDATED=0
usage() {
@ -10,13 +15,14 @@ usage() {
Usage: domoticzctl <command>
Commands:
install Install prerequisites, prepare directories, pull image
install Create LXC container, download Domoticz, configure
uninstall Remove container (preserves data)
check Run prerequisite checks
update Pull new image and restart
status Show container status
logs [-f] Show container logs (use -f to follow)
configure-mqtt Auto-configure Mosquitto broker and Domoticz MQTT bridge
update Download latest Domoticz release and restart
status Show container and service status
logs [N] Show last N lines of logs (default: 50)
shell Open interactive shell in container
configure-mqtt Auto-configure Mosquitto broker and MQTT bridge
configure-haproxy Register as HAProxy vhost for reverse proxy
backup [path] Backup Domoticz data
restore <path> Restore Domoticz from backup
@ -26,6 +32,8 @@ Commands:
USAGE
}
# ---------- helpers ----------
require_root() { [ "$(id -u)" -eq 0 ]; }
uci_get() {
@ -33,13 +41,8 @@ uci_get() {
uci -q get ${CONFIG}.${section}.$1
}
defaults() {
image="$(uci_get image || echo domoticz/domoticz:latest)"
data_path="$(uci_get data_path || echo /srv/domoticz)"
devices_path="$(uci_get devices_path || echo /srv/devices)"
port="$(uci_get port || echo 8080)"
timezone="$(uci_get timezone || echo UTC)"
}
log_info() { echo "[INFO] $*"; logger -t domoticzctl "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t domoticzctl -p err "$*"; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
@ -55,60 +58,450 @@ ensure_packages() {
done
}
check_prereqs() {
defaults() {
data_path="$(uci_get data_path || echo $DATA_PATH_DEFAULT)"
devices_path="$(uci_get devices_path || echo /srv/devices)"
port="$(uci_get port || echo 8080)"
timezone="$(uci_get timezone || echo UTC)"
}
detect_arch() {
case "$(uname -m)" in
aarch64) echo "aarch64" ;;
armv7l) echo "armv7" ;;
x86_64) echo "x86_64" ;;
*) echo "x86_64" ;;
esac
}
# ---------- LXC helpers ----------
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONF" ] && [ -d "$LXC_ROOTFS" ]
}
lxc_exec() {
lxc-attach -n "$LXC_NAME" -- "$@"
}
lxc_stop() {
if lxc_running; then
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 1
fi
}
# ---------- rootfs creation ----------
lxc_create_rootfs() {
local arch=$(detect_arch)
# Map to Debian architecture names
local debian_arch
case "$arch" in
aarch64) debian_arch="arm64" ;;
armv7) debian_arch="armhf" ;;
x86_64) debian_arch="amd64" ;;
*) debian_arch="amd64" ;;
esac
# Use debootstrap if available, otherwise download pre-built rootfs
ensure_dir "$LXC_ROOTFS"
# Minimal Debian rootfs via tarball from LXC image server
local rootfs_url="https://images.linuxcontainers.org/images/debian/bookworm/${debian_arch}/default/"
log_info "Downloading Debian bookworm rootfs for ${debian_arch}..."
# Get latest build directory
local latest_path
latest_path=$(wget -q -O - "$rootfs_url" 2>/dev/null | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | tail -1)
if [ -z "$latest_path" ]; then
log_error "Failed to find latest Debian rootfs build"
return 1
fi
local tarball="/tmp/debian-domoticz.tar.xz"
local tarball_url="${rootfs_url}${latest_path}/rootfs.tar.xz"
wget -q -O "$tarball" "$tarball_url" || {
log_error "Failed to download Debian rootfs from $tarball_url"
return 1
}
tar -xJf "$tarball" -C "$LXC_ROOTFS" || {
log_error "Failed to extract Debian rootfs"
return 1
}
rm -f "$tarball"
# DNS
cp /etc/resolv.conf "$LXC_ROOTFS/etc/resolv.conf" 2>/dev/null || \
echo "nameserver 127.0.0.1" > "$LXC_ROOTFS/etc/resolv.conf"
# Create /dev/null for apt operations
[ -c "$LXC_ROOTFS/dev/null" ] || mknod -m 666 "$LXC_ROOTFS/dev/null" c 1 3 2>/dev/null
# Install runtime dependencies via apt
log_info "Installing Domoticz runtime dependencies..."
chroot "$LXC_ROOTFS" /bin/sh -c "
apt-get -o Acquire::AllowInsecureRepositories=true update -qq 2>/dev/null && \
apt-get --allow-unauthenticated install -y -qq --no-install-recommends \
libcurl3-gnutls libusb-0.1-4 libusb-1.0-0 \
python3-minimal libsqlite3-0 tzdata ca-certificates \
2>&1 | tail -5
" || {
log_error "Failed to install runtime dependencies"
return 1
}
# Rebuild linker cache
chroot "$LXC_ROOTFS" ldconfig 2>/dev/null
# Clean apt cache
chroot "$LXC_ROOTFS" /bin/sh -c "apt-get clean; rm -rf /var/lib/apt/lists/*" 2>/dev/null
log_info "Debian rootfs created."
}
domoticz_download() {
local arch=$(detect_arch)
local domoticz_arch="$arch"
# Domoticz release URLs use different arch names
case "$arch" in
aarch64) domoticz_arch="aarch64" ;;
armv7) domoticz_arch="armv7l" ;;
x86_64) domoticz_arch="x86_64" ;;
esac
local release_url="https://github.com/domoticz/domoticz/releases/latest/download/domoticz_linux_${domoticz_arch}.tgz"
local tarball="/tmp/domoticz-release.tgz"
log_info "Downloading Domoticz release for ${domoticz_arch}..."
wget -q -O "$tarball" "$release_url" || {
log_error "Failed to download Domoticz release"
return 1
}
ensure_dir "$LXC_ROOTFS/opt/domoticz"
tar -xzf "$tarball" -C "$LXC_ROOTFS/opt/domoticz" || {
log_error "Failed to extract Domoticz"
return 1
}
rm -f "$tarball"
chmod +x "$LXC_ROOTFS/opt/domoticz/domoticz" 2>/dev/null
log_info "Domoticz binary installed."
}
# ---------- LXC config generation ----------
lxc_create_config() {
defaults
ensure_dir "$data_path"
[ -d /sys/fs/cgroup ] || { echo "[ERROR] /sys/fs/cgroup missing" >&2; return 1; }
ensure_packages dockerd docker containerd
/etc/init.d/dockerd enable >/dev/null 2>&1
/etc/init.d/dockerd start >/dev/null 2>&1
local arch=$(detect_arch)
local mem_limit=$(uci_get memory_limit || echo "512M")
# Convert memory limit to bytes
local mem_bytes
case "$mem_limit" in
*M) mem_bytes=$(( ${mem_limit%M} * 1048576 )) ;;
*G) mem_bytes=$(( ${mem_limit%G} * 1073741824 )) ;;
*) mem_bytes=536870912 ;;
esac
ensure_dir "$data_path/config"
ensure_dir "$data_path/db"
ensure_dir "$data_path/scripts"
ensure_dir "$data_path/backups"
cat > "$LXC_CONF" << EOF
# Domoticz LXC Container
lxc.uts.name = domoticz
lxc.rootfs.path = dir:${LXC_ROOTFS}
lxc.arch = ${arch}
# Host network (no veth isolation)
lxc.net.0.type = none
# Auto-mounts
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
# Data persistence
lxc.mount.entry = ${data_path}/config opt/domoticz/config none bind,create=dir 0 0
lxc.mount.entry = ${data_path}/db opt/domoticz/db none bind,create=dir 0 0
lxc.mount.entry = ${data_path}/scripts opt/domoticz/scripts none bind,create=dir 0 0
lxc.mount.entry = ${data_path}/backups opt/domoticz/backups none bind,create=dir 0 0
EOF
# USB device passthrough
if [ -d "$devices_path" ]; then
ensure_dir "$LXC_ROOTFS/devices"
cat >> "$LXC_CONF" << EOF
# Device passthrough
lxc.mount.entry = ${devices_path} devices none bind,create=dir 0 0
EOF
fi
# USB serial device passthrough (for Z-Wave/Zigbee dongles)
for dev in /dev/ttyUSB* /dev/ttyACM*; do
if [ -c "$dev" ]; then
local devname=$(basename "$dev")
local major=$(stat -c '%t' "$dev" 2>/dev/null)
major=$((0x${major:-bc}))
cat >> "$LXC_CONF" << EOF
lxc.cgroup2.devices.allow = c ${major}:* rwm
lxc.mount.entry = ${dev} dev/${devname} none bind,create=file 0 0
EOF
fi
done
cat >> "$LXC_CONF" << EOF
# Terminal
lxc.tty.max = 0
lxc.pty.max = 256
# Standard character devices
lxc.cgroup2.devices.allow = c 1:* rwm
lxc.cgroup2.devices.allow = c 5:* rwm
lxc.cgroup2.devices.allow = c 136:* rwm
# Security
lxc.cap.drop = sys_module mac_admin mac_override sys_time
# Disable default seccomp
lxc.seccomp.profile =
# Resource limits
lxc.cgroup2.memory.max = ${mem_bytes}
# Init
lxc.init.cmd = /opt/start-domoticz.sh
EOF
log_info "LXC config generated."
}
pull_image() { defaults; docker pull "$image"; }
# ---------- startup script ----------
stop_container() {
docker stop "$CONTAINER" >/dev/null 2>&1 || true
docker rm "$CONTAINER" >/dev/null 2>&1 || true
generate_start_script() {
defaults
local tz="$timezone"
# Write start script - use quoted heredoc to prevent variable expansion,
# then sed in the runtime values
cat > "$LXC_ROOTFS/opt/start-domoticz.sh" << 'STARTEOF'
#!/bin/sh
export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
cd /opt/domoticz
# Set timezone
TZ_FILE="/usr/share/zoneinfo/__TZ__"
if [ -f "$TZ_FILE" ]; then
cp "$TZ_FILE" /etc/localtime
echo "__TZ__" > /etc/timezone
fi
export TZ="__TZ__"
# Ensure database directory exists
mkdir -p /opt/domoticz/db
# Trap signals and forward to domoticz (PID 1 can't exec directly)
trap 'kill $CHILD 2>/dev/null; wait $CHILD' TERM INT
# Run Domoticz as child process (not PID 1)
./domoticz -www __PORT__ -dbase /opt/domoticz/db/domoticz.db \
-userdata /opt/domoticz/config/ \
-log /opt/domoticz/domoticz.log \
-sslwww 0 &
CHILD=$!
wait $CHILD
STARTEOF
# Replace placeholders with actual values
sed -i "s|__TZ__|${tz}|g" "$LXC_ROOTFS/opt/start-domoticz.sh"
sed -i "s|__PORT__|${port}|g" "$LXC_ROOTFS/opt/start-domoticz.sh"
chmod +x "$LXC_ROOTFS/opt/start-domoticz.sh"
}
# ---------- commands ----------
cmd_install() {
require_root || { echo "Root required" >&2; exit 1; }
check_prereqs || exit 1
ensure_dir "$data_path/config"
pull_image || exit 1
require_root || { log_error "Root required"; exit 1; }
if lxc_exists; then
log_info "Container already exists. Use 'update' to refresh Domoticz."
return 0
fi
# Ensure LXC is available
ensure_packages lxc lxc-common || {
log_error "Failed to install LXC packages"
exit 1
}
defaults
ensure_dir "$LXC_PATH/$LXC_NAME"
ensure_dir "$data_path"
lxc_create_rootfs || exit 1
domoticz_download || exit 1
lxc_create_config
generate_start_script
uci set ${CONFIG}.main.enabled='1'
uci commit ${CONFIG}
/etc/init.d/domoticz enable
echo "Domoticz installed. Start with /etc/init.d/domoticz start"
log_info "Domoticz installed in LXC container. Start with /etc/init.d/domoticz start"
}
cmd_uninstall() {
require_root || { echo "Root required" >&2; exit 1; }
require_root || { log_error "Root required"; exit 1; }
/etc/init.d/domoticz stop >/dev/null 2>&1
stop_container
lxc_stop
defaults
docker rmi "$image" >/dev/null 2>&1 || true
# Remove container rootfs but preserve data
rm -rf "$LXC_PATH/$LXC_NAME"
uci set ${CONFIG}.main.enabled='0'
uci commit ${CONFIG}
/etc/init.d/domoticz disable >/dev/null 2>&1
echo "Domoticz uninstalled. Data preserved at ${data_path}."
log_info "Domoticz container removed. Data preserved at ${data_path}."
}
cmd_check() { check_prereqs; echo "Prerequisite check completed."; }
cmd_check() {
require_root || { log_error "Root required"; exit 1; }
echo "=== Prerequisites ==="
# LXC
if command -v lxc-start >/dev/null 2>&1; then
echo "[OK] lxc-start available"
else
echo "[MISSING] lxc-start — install lxc"
fi
# cgroup
if [ -d /sys/fs/cgroup ]; then
echo "[OK] /sys/fs/cgroup present"
else
echo "[MISSING] /sys/fs/cgroup"
fi
# Container
if lxc_exists; then
echo "[OK] Container rootfs exists"
else
echo "[MISSING] Container not installed — run 'domoticzctl install'"
fi
# Container state
if lxc_running; then
echo "[OK] Container is RUNNING"
else
echo "[STOPPED] Container is not running"
fi
echo ""
echo "=== USB Devices ==="
for dev in /dev/ttyUSB* /dev/ttyACM*; do
[ -e "$dev" ] && echo " $dev"
done
[ ! -e /dev/ttyUSB0 ] && [ ! -e /dev/ttyACM0 ] && echo " (none detected)"
echo ""
echo "=== Mosquitto ==="
if pgrep mosquitto >/dev/null 2>&1; then
echo "[OK] Mosquitto running"
else
echo "[STOPPED] Mosquitto not running"
fi
echo ""
echo "=== Zigbee2MQTT ==="
if [ -f /srv/zigbee2mqtt/alpine/rootfs/run.pid ] || pgrep -f zigbee2mqtt >/dev/null 2>&1; then
echo "[OK] Zigbee2MQTT running"
else
echo "[STOPPED] Zigbee2MQTT not running"
fi
}
cmd_update() {
require_root || { echo "Root required" >&2; exit 1; }
pull_image || exit 1
require_root || { log_error "Root required"; exit 1; }
if ! lxc_exists; then
log_error "Container not installed. Run 'domoticzctl install' first."
exit 1
fi
lxc_stop
domoticz_download || exit 1
generate_start_script
/etc/init.d/domoticz restart
echo "Domoticz updated and restarted."
log_info "Domoticz updated and restarted."
}
cmd_status() { docker ps -a --filter "name=$CONTAINER"; }
cmd_status() {
defaults
echo "=== Domoticz Status ==="
cmd_logs() { docker logs "$@" "$CONTAINER"; }
if lxc_running; then
echo "Container: RUNNING"
lxc-info -n "$LXC_NAME" 2>/dev/null | grep -E "PID|Memory|CPU"
elif lxc_exists; then
echo "Container: STOPPED (installed)"
else
echo "Container: NOT INSTALLED"
fi
echo ""
echo "Port: ${port}"
echo "Data: ${data_path}"
if [ -d "$data_path/db" ]; then
local db_size=$(du -sh "$data_path/db" 2>/dev/null | cut -f1)
echo "DB size: ${db_size:-0}"
fi
}
cmd_logs() {
defaults
local lines="${1:-50}"
local logfile="$data_path/domoticz.log"
if [ -f "$logfile" ]; then
tail -n "$lines" "$logfile"
elif lxc_running; then
lxc_exec tail -n "$lines" /opt/domoticz/domoticz.log 2>/dev/null || echo "No logs available."
else
echo "No logs available."
fi
}
cmd_shell() {
require_root || { log_error "Root required"; exit 1; }
if ! lxc_running; then
log_error "Container is not running. Start it first."
exit 1
fi
lxc_exec /bin/sh
}
cmd_configure_mqtt() {
require_root || { echo "Root required" >&2; exit 1; }
require_root || { log_error "Root required"; exit 1; }
local broker=$(uci_get broker mqtt)
local broker_port=$(uci_get broker_port mqtt)
@ -122,9 +515,9 @@ cmd_configure_mqtt() {
# Ensure Mosquitto is installed and running
if ! command -v mosquitto >/dev/null 2>&1; then
echo "Installing mosquitto-nossl..."
log_info "Installing mosquitto-nossl..."
ensure_packages mosquitto-nossl mosquitto-client-nossl || {
echo "[ERROR] Failed to install Mosquitto" >&2
log_error "Failed to install Mosquitto"
return 1
}
fi
@ -132,7 +525,6 @@ cmd_configure_mqtt() {
# Configure Mosquitto listener
local mosquitto_conf="/etc/mosquitto/mosquitto.conf"
if [ -f "$mosquitto_conf" ]; then
# Ensure listener on configured port
if ! grep -q "^listener ${broker_port}" "$mosquitto_conf" 2>/dev/null; then
echo "" >> "$mosquitto_conf"
echo "# Auto-configured by domoticzctl" >> "$mosquitto_conf"
@ -156,7 +548,7 @@ cmd_configure_mqtt() {
done
if ! netstat -tln 2>/dev/null | grep -q ":${broker_port} "; then
echo "[WARN] Mosquitto may not be listening on port ${broker_port}" >&2
log_info "Mosquitto may not be listening on port ${broker_port}"
fi
# Check zigbee2mqtt MQTT settings
@ -164,7 +556,7 @@ cmd_configure_mqtt() {
local z2m_mqtt_uri=$(uci -q get zigbee2mqtt.main.mqtt_host)
local z2m_base=$(uci -q get zigbee2mqtt.main.base_topic)
# z2m stores full URI (mqtt://host:port) — extract host and port
# z2m stores full URI (mqtt://host:port)
local z2m_host=$(echo "$z2m_mqtt_uri" | sed 's|^mqtt[s]*://||' | cut -d: -f1)
local z2m_port_conf=$(echo "$z2m_mqtt_uri" | sed 's|^mqtt[s]*://||' | cut -d: -f2)
z2m_host="${z2m_host:-127.0.0.1}"
@ -190,9 +582,6 @@ cmd_configure_mqtt() {
uci set ${CONFIG}.mqtt.z2m_topic="$z2m_topic"
uci commit ${CONFIG}
# Note: Domoticz MQTT configuration happens inside the Domoticz web UI
# (Hardware > MQTT Client Gateway). We configure the broker and topic,
# but the user must add the MQTT hardware in Domoticz UI.
echo ""
echo "MQTT bridge configured:"
echo " Broker: ${broker}:${broker_port}"
@ -207,129 +596,137 @@ cmd_configure_mqtt() {
}
cmd_configure_haproxy() {
require_root || { echo "Root required" >&2; exit 1; }
require_root || { log_error "Root required"; exit 1; }
local domain=$(uci_get domain network)
local port_val=$(uci_get port)
domain="${domain:-domoticz.secubox.local}"
port_val="${port_val:-8080}"
# Use haproxyctl if available
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl add-vhost "$domain" "127.0.0.1:${port_val}" 2>&1
local code=$?
if [ $code -eq 0 ]; then
uci set ${CONFIG}.network.haproxy='1'
uci commit ${CONFIG}
echo "HAProxy vhost configured for ${domain} -> 127.0.0.1:${port_val}"
log_info "HAProxy vhost configured for ${domain} -> 127.0.0.1:${port_val}"
else
echo "[ERROR] haproxyctl add-vhost failed" >&2
log_error "haproxyctl add-vhost failed"
return 1
fi
else
echo "[ERROR] haproxyctl not found — install secubox-app-haproxy first" >&2
log_error "haproxyctl not found — install secubox-app-haproxy first"
return 1
fi
}
cmd_backup() {
require_root || { echo "Root required" >&2; exit 1; }
require_root || { log_error "Root required"; exit 1; }
defaults
local backup_path="${1:-/tmp/domoticz-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
if [ ! -d "$data_path/config" ]; then
echo "[ERROR] No data to backup at ${data_path}/config" >&2
if [ ! -d "$data_path" ]; then
log_error "No data to backup at ${data_path}"
return 1
fi
tar czf "$backup_path" -C "$data_path" config 2>&1
tar czf "$backup_path" -C "$data_path" . 2>&1
local code=$?
if [ $code -eq 0 ]; then
echo "Backup created: ${backup_path}"
log_info "Backup created: ${backup_path}"
else
echo "[ERROR] Backup failed" >&2
log_error "Backup failed"
return 1
fi
}
cmd_restore() {
require_root || { echo "Root required" >&2; exit 1; }
require_root || { log_error "Root required"; exit 1; }
defaults
local backup_path="$1"
if [ -z "$backup_path" ] || [ ! -f "$backup_path" ]; then
echo "[ERROR] Backup file not found: ${backup_path}" >&2
log_error "Backup file not found: ${backup_path}"
return 1
fi
# Stop service before restore
/etc/init.d/domoticz stop >/dev/null 2>&1
stop_container
lxc_stop
ensure_dir "$data_path"
tar xzf "$backup_path" -C "$data_path" 2>&1
local code=$?
if [ $code -eq 0 ]; then
echo "Restored from ${backup_path}"
log_info "Restored from ${backup_path}"
echo "Restart Domoticz with: /etc/init.d/domoticz start"
else
echo "[ERROR] Restore failed" >&2
log_error "Restore failed"
return 1
fi
}
cmd_mesh_register() {
require_root || { echo "Root required" >&2; exit 1; }
require_root || { log_error "Root required"; exit 1; }
defaults
if command -v secubox-p2p >/dev/null 2>&1; then
secubox-p2p register-service domoticz "$port" 2>&1
uci set ${CONFIG}.mesh.enabled='1'
uci commit ${CONFIG}
echo "Domoticz registered in P2P mesh on port ${port}"
log_info "Domoticz registered in P2P mesh on port ${port}"
else
echo "[ERROR] secubox-p2p not found — install secubox-p2p first" >&2
log_error "secubox-p2p not found — install secubox-p2p first"
return 1
fi
}
cmd_service_run() {
require_root || { echo "Root required" >&2; exit 1; }
check_prereqs || exit 1
defaults
stop_container
require_root || { log_error "Root required"; exit 1; }
local docker_args="--name $CONTAINER -p ${port}:8080 -v $data_path/config:/config"
[ -d "$devices_path" ] && docker_args="$docker_args -v $devices_path:/devices"
# If MQTT enabled, pass broker info to container network
local mqtt_en=$(uci_get enabled mqtt)
if [ "${mqtt_en:-0}" = "1" ]; then
docker_args="$docker_args --add-host=mqtt-broker:$(uci_get broker mqtt || echo 127.0.0.1)"
if ! lxc_exists; then
log_error "Container not installed. Run 'domoticzctl install' first."
exit 1
fi
exec docker run --rm $docker_args -e TZ="$timezone" "$image"
defaults
# Regenerate config and startup script on each run (picks up UCI changes)
lxc_create_config
generate_start_script
# Stop any previous instance
lxc_stop
# Update DNS
cp /etc/resolv.conf "$LXC_ROOTFS/etc/resolv.conf" 2>/dev/null
log_info "Starting Domoticz LXC container..."
exec lxc-start -n "$LXC_NAME" -F
}
cmd_service_stop() {
require_root || { echo "Root required" >&2; exit 1; }
stop_container
require_root || { log_error "Root required"; exit 1; }
lxc_stop
log_info "Domoticz container stopped."
}
# ---------- main ----------
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
configure-mqtt) shift; cmd_configure_mqtt "$@" ;;
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
configure-mqtt) shift; cmd_configure_mqtt "$@" ;;
configure-haproxy) shift; cmd_configure_haproxy "$@" ;;
backup) shift; cmd_backup "$@" ;;
restore) shift; cmd_restore "$@" ;;
mesh-register) shift; cmd_mesh_register "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
backup) shift; cmd_backup "$@" ;;
restore) shift; cmd_restore "$@" ;;
mesh-register) shift; cmd_mesh_register "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
*) log_error "Unknown command: $1"; usage >&2; exit 1 ;;
esac

View File

@ -438,11 +438,20 @@ lxc.mount.entry = $data_path/logs srv/logs none bind,create=dir 0 0
lxc.environment = STREAMLIT_THEME_BASE=$theme_base
lxc.environment = STREAMLIT_THEME_PRIMARY=$theme_primary
# Terminal
lxc.tty.max = 0
lxc.pty.max = 256
# Standard character devices
lxc.cgroup2.devices.allow = c 1:* rwm
lxc.cgroup2.devices.allow = c 5:* rwm
lxc.cgroup2.devices.allow = c 136:* rwm
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
# Resource limits
lxc.cgroup.memory.limit_in_bytes = $mem_bytes
lxc.cgroup2.memory.max = $mem_bytes
# Init command
lxc.init.cmd = /opt/start-streamlit.sh

View File

@ -1522,7 +1522,7 @@
"name": "Domoticz",
"version": "1.0.0",
"category": "iot",
"runtime": "docker",
"runtime": "lxc",
"description": "Home automation system with MQTT bridge, Zigbee2MQTT integration, and IoT device management",
"author": "CyberMind.fr",
"license": "GPL-3.0",
@ -1532,7 +1532,7 @@
"home-automation",
"iot",
"smart-home",
"docker",
"lxc",
"mqtt",
"zigbee",
"mesh"
@ -1541,8 +1541,8 @@
"required": [
"secubox-app-domoticz",
"luci-app-domoticz",
"docker",
"dockerd"
"lxc",
"lxc-common"
],
"optional": [
"mosquitto-nossl",
@ -1562,18 +1562,20 @@
"min_storage_mb": 100
},
"status": "stable",
"pkg_version": "1.0.0-3",
"pkg_version": "1.0.0-4",
"app_version": "1.0.0",
"changelog": {
"1.0.0-3": {
"1.0.0-4": {
"date": "2026-02-04",
"changes": [
"Rewrite from Docker to LXC Alpine container",
"Native Domoticz binary download from releases.domoticz.com",
"LXC config with USB cgroup2 passthrough and memory limits",
"LuCI dashboard with IoT integration status",
"MQTT auto-bridge for Mosquitto and Zigbee2MQTT",
"HAProxy reverse proxy integration",
"P2P mesh service registration",
"Backup and restore support",
"USB device passthrough visibility"
"Backup and restore support"
]
},
"1.0.0": {