#!/usr/bin/env bash # # Smart Home Container Host Setup # Target: Debian 12 (Bookworm) or Raspberry Pi OS Lite (Bookworm-based) # # Sets up Docker + a compose stack for everything that lives on this single # host per docs/project-plan.md Phase 1 and Phase 9 (the LLM/GPU host is a # separate physical machine — see hosts/llm-host): # - Home Assistant (Container install) # - Mosquitto (MQTT broker) # - Zigbee2MQTT (talks to your USB-attached CC2652P/Haozee coordinator) # - Node-RED (automation glue, identity-correlation flow home) # - Frigate (NVR + face recognition, for the peephole cam) # - Grocy (self-hosted inventory / shopping list) # - Mealie (optional meal planning) # - Netdata (host + container monitoring) # - Homepage (single dashboard landing page) # - ntfy (self-hosted push notifications) # - Portainer (Docker GUI) # - restic scheduled backups (optional, off by default) # # Run as: sudo ./setup-container-host.sh # # EDIT THE VARIABLES BELOW BEFORE RUNNING. set -euo pipefail # --------------------------------------------------------------------------- # CONFIGURATION — edit these before running # --------------------------------------------------------------------------- BASE_DIR="/opt/smart-home" # Where all container config/data will live TIMEZONE="Europe/Vienna" # Adjust to your timezone ENABLE_MEALIE="false" # Set to "true" to also deploy Mealie ENABLE_INTEL_HWACCEL="false" # Set to "true" if this host has an Intel iGPU for Frigate # --- Phase 1/9 add-ons — on by default, set to "false" to skip any of them --- ENABLE_NODERED="true" ENABLE_NETDATA="true" ENABLE_HOMEPAGE="true" ENABLE_NTFY="true" ENABLE_PORTAINER="true" # --- Scheduled backups (restic) — off by default until you pick a target --- # Set ENABLE_BACKUPS=true and RESTIC_REPOSITORY to a local path (e.g. an # external/USB drive mount, or a NAS mount), or a remote target restic # supports (s3:..., sftp:..., b2:..., rest:...). See https://restic.net ENABLE_BACKUPS="false" RESTIC_REPOSITORY="/mnt/backup/smart-home-restic" BACKUP_SCHEDULE="03:30" # systemd OnCalendar time, daily at this local time # --- Zigbee adapter: USB (CC2652P/CH340C, e.g. Haozee/Sonoff Dongle-P style) --- # Run `ls -l /dev/serial/by-id/` AFTER plugging the adapter in, and paste the # full path it shows here. This is more stable across reboots than /dev/ttyUSB0. # Example: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 ZIGBEE_USB_DEVICE="/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0" # --------------------------------------------------------------------------- # Sanity checks # --------------------------------------------------------------------------- if [[ $EUID -ne 0 ]]; then echo "Please run as root (sudo ./setup-container-host.sh)" >&2 exit 1 fi if ! grep -qi "debian" /etc/os-release; then echo "Warning: this script targets Debian/Raspberry Pi OS. Proceeding anyway..." fi if [[ ! -e "$ZIGBEE_USB_DEVICE" ]]; then echo "Warning: $ZIGBEE_USB_DEVICE does not exist yet." echo " Plug in your Zigbee adapter and run 'ls -l /dev/serial/by-id/' to find the correct path," echo " then edit ZIGBEE_USB_DEVICE at the top of this script before re-running." echo " Continuing anyway — the zigbee2mqtt container just won't start correctly until this is fixed." fi if [[ "$ENABLE_BACKUPS" == "true" && "$RESTIC_REPOSITORY" == "/mnt/backup/smart-home-restic" ]]; then echo "Warning: ENABLE_BACKUPS=true but RESTIC_REPOSITORY is still the placeholder path." echo " Edit RESTIC_REPOSITORY at the top of this script to point at real backup storage." fi HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" HOST_IP="${HOST_IP:-}" echo "=== Smart Home Container Host Setup ===" echo "Base directory: $BASE_DIR" echo "Timezone: $TIMEZONE" echo "Host IP (detected): $HOST_IP" echo "Zigbee adapter (USB): ${ZIGBEE_USB_DEVICE}" echo # --------------------------------------------------------------------------- # 1. System update + prerequisites # --------------------------------------------------------------------------- echo "--- Updating system and installing prerequisites ---" apt-get update apt-get upgrade -y apt-get install -y \ ca-certificates \ curl \ gnupg \ lsb-release \ jq \ openssl if [[ "$ENABLE_BACKUPS" == "true" ]]; then apt-get install -y restic fi # --------------------------------------------------------------------------- # 2. Install Docker Engine + Compose plugin (official Docker repo) # --------------------------------------------------------------------------- if ! command -v docker &> /dev/null; then echo "--- Installing Docker Engine ---" install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc ARCH="$(dpkg --print-architecture)" CODENAME="$(. /etc/os-release && echo "$VERSION_CODENAME")" echo \ "deb [arch=${ARCH} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${CODENAME} stable" \ > /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Allow the invoking (non-root) user to run docker without sudo, if applicable if [[ -n "${SUDO_USER:-}" ]]; then usermod -aG docker "$SUDO_USER" echo "Added $SUDO_USER to the docker group. Log out/in for it to take effect." fi else echo "--- Docker already installed, skipping ---" fi # --------------------------------------------------------------------------- # 3. Directory structure # --------------------------------------------------------------------------- echo "--- Creating directory structure under $BASE_DIR ---" mkdir -p "$BASE_DIR"/{homeassistant,mosquitto/config,mosquitto/data,mosquitto/log,zigbee2mqtt/data,frigate/config,frigate/media,grocy/config,grocy/data} if [[ "$ENABLE_MEALIE" == "true" ]]; then mkdir -p "$BASE_DIR"/mealie/data fi if [[ "$ENABLE_NODERED" == "true" ]]; then mkdir -p "$BASE_DIR"/nodered/data fi if [[ "$ENABLE_NETDATA" == "true" ]]; then mkdir -p "$BASE_DIR"/netdata/{config,lib,cache} fi if [[ "$ENABLE_HOMEPAGE" == "true" ]]; then mkdir -p "$BASE_DIR"/homepage/config fi if [[ "$ENABLE_NTFY" == "true" ]]; then mkdir -p "$BASE_DIR"/ntfy/{data,config} fi if [[ "$ENABLE_PORTAINER" == "true" ]]; then mkdir -p "$BASE_DIR"/portainer/data fi # --------------------------------------------------------------------------- # 4. Mosquitto config # --------------------------------------------------------------------------- echo "--- Writing Mosquitto config ---" cat > "$BASE_DIR/mosquitto/config/mosquitto.conf" <<'EOF' # Basic internal-network broker config. # This trusts anything on your Docker/LAN network. If Mosquitto will ever be # reachable beyond your trusted LAN, add password_file auth before exposing it. listener 1883 allow_anonymous true persistence true persistence_location /mosquitto/data/ log_dest file /mosquitto/log/mosquitto.log EOF # --------------------------------------------------------------------------- # 5. Zigbee2MQTT config (pre-seeded for your USB CC2652P dongle) # --------------------------------------------------------------------------- echo "--- Writing Zigbee2MQTT config ---" cat > "$BASE_DIR/zigbee2mqtt/data/configuration.yaml" < "$BASE_DIR/frigate/config/config.yml" <<'EOF' mqtt: host: mosquitto port: 1883 # Face recognition (Frigate 0.16+) face_recognition: enabled: true cameras: peephole: ffmpeg: inputs: - path: rtsp://USERNAME:PASSWORD@CAMERA_IP:554/STREAM_PATH roles: - detect - record detect: width: 1280 height: 720 fps: 5 record: enabled: true # Uncomment and adjust if using Intel QuickSync/OpenVINO hardware acceleration: # ffmpeg: # hwaccel_args: preset-vaapi # detectors: # ov: # type: openvino # device: GPU EOF echo " NOTE: edit $BASE_DIR/frigate/config/config.yml with your real camera RTSP URL before starting Frigate." # --------------------------------------------------------------------------- # 7. Homepage dashboard config (single landing page over the whole stack) # --------------------------------------------------------------------------- if [[ "$ENABLE_HOMEPAGE" == "true" ]]; then echo "--- Writing Homepage config ---" cat > "$BASE_DIR/homepage/config/settings.yaml" <<'EOF' title: Smart Home theme: dark color: slate headerStyle: clean EOF cat > "$BASE_DIR/homepage/config/widgets.yaml" <<'EOF' - resources: cpu: true memory: true disk: / EOF cat > "$BASE_DIR/homepage/config/bookmarks.yaml" <<'EOF' [] EOF cat > "$BASE_DIR/homepage/config/services.yaml" < "$BASE_DIR/docker-compose.yml" < "$RESTIC_PASSWORD_FILE" chmod 600 "$RESTIC_PASSWORD_FILE" echo " Generated a new restic repository password at $RESTIC_PASSWORD_FILE." echo " BACK THIS FILE UP SOMEWHERE ELSE — losing it makes the backup repo unreadable." fi cat > "$BASE_DIR/backup.env" < "$BASE_DIR/backup.sh" <<'BACKUP_EOF' #!/usr/bin/env bash # Backs up all stateful smart-home volumes with restic. # Stops the stack briefly for a consistent snapshot of SQLite-backed configs # (HA, Zigbee2MQTT, Grocy, Node-RED), then restarts it. # Frigate's recorded video is excluded — it's large and non-critical to keep; # face-recognition embeddings live under frigate/config, which IS backed up. set -euo pipefail BASE_DIR="/opt/smart-home" source "$BASE_DIR/backup.env" cd "$BASE_DIR" if ! restic snapshots --no-lock >/dev/null 2>&1; then echo "Initializing new restic repository at $RESTIC_REPOSITORY" restic init fi echo "Stopping stack for a consistent backup..." docker compose stop restic backup "$BASE_DIR" \ --exclude "$BASE_DIR/frigate/media" \ --exclude "$BASE_DIR/.restic-password" \ --exclude "$BASE_DIR/backup.env" echo "Restarting stack..." docker compose start restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune BACKUP_EOF chmod +x "$BASE_DIR/backup.sh" cat > /etc/systemd/system/smart-home-backup.service < /etc/systemd/system/smart-home-backup.timer <