Home Assistant Docker Compose for Homelab 2026: From Container to Smart Home Stack
Reading time: ~14 minutes Audience: Homelab owners who want a self-hosted smart home without vendor lock-in
Introduction: Docker Compose for Home Assistant — Why It Makes Sense in 2026
Home Assistant has grown from a hobbyist project into the most popular open-source smart home platform on the planet. The r/homeassistant subreddit sits at over 250,000 members, and every major smart device manufacturer now either supports Home Assistant directly or has a community integration that works better than the official app. But there is one persistent debate that trips up newcomers: should you run Home Assistant OS (HAOS), Home Assistant Container (Docker), or Home Assistant Core (bare Python)?
If you already run a homelab — Proxmox, a mini PC, or even a Raspberry Pi 5 with a handful of Docker containers — the answer leans strongly toward Docker Compose. Here is why: you already have Docker running for your media server, reverse proxy, DNS filter, and monitoring stack. Adding Home Assistant as another Compose service keeps your infrastructure uniform, your management tooling consistent, and your backup strategy identical across every service.
This guide covers the complete stack: Home Assistant Core in Docker, Mosquitto MQTT, Zigbee2MQTT for USB dongle integration, an optional Node-RED automation sidecar, reverse proxy with Nginx Proxy Manager, remote access via Cloudflare Tunnel, and Proxmox LXC USB passthrough. Every code block is copy-paste ready for 2026.
Why Docker Compose for Home Assistant in a Homelab?
Before writing a single YAML line, you need to understand the tradeoffs. Home Assistant offers three official installation methods, and they are not interchangeable.
HAOS vs Docker Compose vs Core: The Decision Tree
| Method | What It Is | Best For | Watch Out For |
|---|---|---|---|
| HAOS | Full OS image (rootfs or VM) | Raspberry Pi dedicated, VM-only homelab | Steals an entire device or VM; can’t run other Docker containers alongside |
| Docker Compose | homeassistant/home-assistant container |
Homelab with existing Docker stack | No add-on store; replace add-ons with separate containers |
| Home Assistant Core | pip install homeassistant |
Python developers, custom integrations | No supervisor; manual OS dependency management |
Choose HAOS if: You want a dedicated appliance, do not run other server workloads, and want the one-click add-on store for things like Node-RED, ESPHome, and DuckDNS.
Choose Docker Compose if: You already run Docker for other services (Jellyfin, Nginx Proxy Manager, Pihole, Grafana), you use Portainer or Dockge for container management, and you are comfortable running companion services as separate containers instead of add-ons.
Choose Core if: You are developing custom integrations in Python and need direct filesystem access without container isolation.
The Add-On Question (and the Docker Workaround)
The number-one reason people hesitate to use Docker is the add-on store. In HAOS, add-ons are pre-packaged Docker containers that Home Assistant manages for you — Node-RED, ESPHome, Mosquitto, Zigbee2MQTT, and file editors all install with one click.
In Docker Compose, you simply run each add-on as a separate container in the same Compose stack. The result is identical — Mosquitto still talks to Home Assistant over the internal Docker network; Zigbee2MQTT still publishes to MQTT — but you manage them through docker compose pull && docker compose up -d instead of the Supervisor UI. For homelabbers already managing 20 containers, this is a feature, not a bug: one workflow, one update cadence, no split-brain management.
For the few add-ons that do not have standalone Docker images (like the Google Drive Backup add-on), you use HACS (Home Assistant Community Store) to install equivalent custom integrations directly inside the Home Assistant container — no Supervisor required.
What You’ll Need
Hardware
| Tier | Hardware | RAM | Storage | Power | Best For |
|---|---|---|---|---|---|
| Budget | Raspberry Pi 5 (8 GB) + 128 GB SSD | 8 GB | 128 GB SSD | 8–12W | Starter smart home (lights, sensors, 1–2 cameras) |
| Standard | Intel N100 Mini PC (Beelink S12 Pro, Minisforum UN100) | 16 GB | 256 GB NVMe | 12–18W | Full smart home + Frigate NVR + 4–8 cameras |
| Enthusiast | Proxmox VM (4 vCPU, 8 GB RAM) on existing homelab server | 8 GB | 64 GB virtual disk | Shared | Consolidation — run HA alongside other VMs/CTs |
| Power User | Intel N305 or AMD Ryzen 5825U Mini PC | 32 GB | 512 GB NVMe | 25–40W | Frigate AI object detection, Whisper voice, Matter controller |
Zigbee/Z-Wave USB Dongle Recommendations (2026):
- Sonoff ZBDongle-P (CC2652P) — ~$15, excellent range, Zigbee2MQTT and ZHA compatible
- ConBee III (RaspBee III for Pi GPIO) — ~$40, deCONZ or Zigbee2MQTT, very mature
- Home Assistant SkyConnect (EFR32MG21) — ~$30, official Nabu Casa hardware, Thread/Matter ready, ZHA native
- Aeotec Z-Stick 7 (Z-Wave 700 series) — ~$50, best Z-Wave dongle, 700-series for long range
Software
- Docker Engine 24+ and Docker Compose v2 (bundled with Docker)
- Git for version-tracking your Compose files
- Static IP or mDNS (Avahi) on the host so integrations can discover it reliably
Project Structure
Create a clean directory on your Docker host. Version-control it with Git — your Compose file is infrastructure as code, and you will thank yourself after the first accidental docker compose down.
~/docker/homeassistant/
├── .env # Version tags and secrets
├── docker-compose.yml # Main stack
├── mosquitto/
│ └── config/
│ └── mosquitto.conf
├── zigbee2mqtt/
│ └── data/
│ └── configuration.yaml
├── node-red/
│ └── data/
└── homeassistant/
└── config/ # Auto-created on first run
The .env file pins exact versions so you control when updates happen:
# .env — Version pins for Home Assistant smart home stack
# Update these manually and review changelogs before bumping
HA_VERSION=2026.6.0
MOSQUITTO_VERSION=2.0.18
Z2M_VERSION=1.37.0
NODE_RED_VERSION=4.0.2
TZ=Asia/Jakarta
Volume strategy: every container gets a bind-mounted directory under ~/docker/homeassistant/. This keeps backups simple — rsync -a ~/docker/homeassistant/ /mnt/nas/backups/homeassistant/ captures your entire smart home config in one command.
The Docker Compose File
Below is the complete docker-compose.yml. Each service is explained after the block.
# docker-compose.yml — Home Assistant Smart Home Stack
# Deploy: docker compose up -d
# Update: docker compose pull && docker compose up -d
services:
# ── Core: Home Assistant ──────────────────────────────
homeassistant:
image: homeassistant/home-assistant:${HA_VERSION}
container_name: homeassistant
restart: unless-stopped
network_mode: host
privileged: true # Required for USB device access
environment:
- TZ=${TZ}
volumes:
- ./homeassistant/config:/config
- /run/dbus:/run/dbus:ro # Bluetooth (optional)
# If using bridge network instead of host, add:
# ports:
# - "8123:8123"
# and comment out network_mode: host
# ── MQTT Broker: Mosquitto ────────────────────────────
mosquitto:
image: eclipse-mosquitto:${MOSQUITTO_VERSION}
container_name: mosquitto
restart: unless-stopped
ports:
- "1883:1883" # MQTT
- "9001:9001" # WebSockets
volumes:
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
# ── Zigbee Bridge: Zigbee2MQTT ────────────────────────
zigbee2mqtt:
image: koenkk/zigbee2mqtt:${Z2M_VERSION}
container_name: zigbee2mqtt
restart: unless-stopped
depends_on:
- mosquitto
devices:
- /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_XXXXXXXX:/dev/ttyUSB0
volumes:
- ./zigbee2mqtt/data:/app/data
environment:
- TZ=${TZ}
# network_mode: host # Uncomment if discovery fails on bridge
# ── Optional: Node-RED Automation ─────────────────────
node-red:
image: nodered/node-red:${NODE_RED_VERSION}
container_name: node-red
restart: unless-stopped
ports:
- "1880:1880"
volumes:
- ./node-red/data:/data
environment:
- TZ=${TZ}
volumes:
mosquitto_data:
mosquitto_log:
Service-by-Service Explanation
Home Assistant (network_mode: host): Using host networking is the officially recommended approach for the Docker container because Home Assistant relies on mDNS (Bonjour/Avahi), UPnP, and DHCP discovery to find devices on your LAN. Without host mode, many auto-discovered integrations (Google Cast, Apple HomeKit, Philips Hue, Sonos) silently break. The privileged: true flag gives the container access to USB devices — essential for Zigbee/Z-Wave dongles. If your security policy forbids host networking, use a bridge network with explicit port mappings and accept that device discovery will be manual.
Mosquitto (bridge network): This is a standard MQTT broker. It runs on the default bridge network so Home Assistant can reach it at mosquitto:1883 (Docker DNS) if needed, but since Home Assistant is on host mode, you configure the MQTT integration with localhost:1883 instead. The bridge mode keeps Mosquitto isolated while still being reachable from the LAN for non-Docker MQTT clients.
Zigbee2MQTT (bridge with device passthrough): The devices: block maps the physical USB dongle into the container. Find your dongle’s stable path with ls -la /dev/serial/by-id/ — this symlink survives reboots, unlike /dev/ttyUSB0 which can change numbering.
Node-RED (bridge network, optional): If you prefer visual flow-based automation over Home Assistant’s built-in YAML automations, Node-RED is the gold standard. It connects to Home Assistant via the Home Assistant WebSocket API and to MQTT via the broker.
Mosquitto Configuration
Create ./mosquitto/config/mosquitto.conf:
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
persistence true
persistence_location /mosquitto/data
log_dest stdout
Set the MQTT credentials (run this after the first docker compose up -d mosquitto):
docker compose exec mosquitto mosquitto_passwd -c /mosquitto/config/passwd homeassistant
Use these credentials when configuring the MQTT integration in Home Assistant.
Zigbee2MQTT Configuration
Create ./zigbee2mqtt/data/configuration.yaml:
homeassistant: true
permit_join: false # Enable only when pairing new devices
mqtt:
server: mqtt://localhost:1883
user: homeassistant
password: your_password_here
serial:
port: /dev/ttyUSB0
adapter: zstack # For Sonoff ZBDongle-P. Use 'deconz' for ConBee
frontend:
port: 8080
advanced:
network_key: GENERATE_A_RANDOM_KEY
Generate the network_key with openssl rand -hex 16 — this encrypts your Zigbee network and must be the same if you ever re-pair devices. Save it somewhere safe.
Step-by-Step Installation
Step 1 — Prepare Your Host
On Proxmox (LXC container):
Create an unprivileged LXC container with Debian 12, enable nesting=1 and keyctl=1 in the container options. Install Docker inside the LXC. For USB passthrough, add the LXC device mapping in Proxmox: select the container → Resources → Add → Device Passthrough → /dev/serial/by-id/usb-Silicon_Labs_... — no reboot needed. If you haven’t set up Proxmox yet, see our Proxmox beginner guide.
On Ubuntu/Debian (bare metal or VM):
# Install Docker (official repo method, 2026)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker --version
docker compose version
On Raspberry Pi 5:
# Same Docker install, then verify ARM architecture
docker info | grep Architecture # Should show aarch64
Pi users: Use an SSD via USB 3.0 or NVMe HAT, not a microSD card. Home Assistant writes recorder data continuously; an SD card will fail within 6–12 months under that write load.
Step 2 — Create Directories and Set Permissions
mkdir -p ~/docker/homeassistant/{homeassistant/config,mosquitto/config,zigbee2mqtt/data,node-red/data}
chown -R 1000:1000 ~/docker/homeassistant/node-red/data
Node-RED runs as UID 1000 by default. Home Assistant and Zigbee2MQTT create their own directory structures on first run.
Step 3 — Write the Docker Compose File
Copy the docker-compose.yml and .env from the sections above into ~/docker/homeassistant/. Adjust your Zigbee dongle path under devices: in the Zigbee2MQTT service. If you don’t have a Zigbee dongle yet, comment out the entire zigbee2mqtt service — you can add it later without rebuilding the stack.
Step 4 — Start the Stack
cd ~/docker/homeassistant
docker compose up -d
# Watch logs to confirm startup
docker compose logs -f homeassistant
Wait 2–3 minutes for the first-run initialization. Home Assistant downloads several Python wheels and builds the frontend on first start. You will know it’s ready when the logs show "Home Assistant initialized in X seconds".
Step 5 — First-Run Configuration
Open http://<host-ip>:8123 in a browser. Home Assistant walks you through:
- Create owner account — This is your admin user. Use a strong password.
- Name your home — Pick the location for timezone, sunrise/sunset, and weather automations.
- Device discovery — Home Assistant auto-scans for devices on your network (printers, Chromecasts, Roku, Philips Hue bridges). Accept or skip.
- Integrations — You will be prompted to install discovered integrations. At minimum, install MQTT and point it to
localhost:1883with the credentials you set in Mosquitto.
Step 6 — Install HACS (Home Assistant Community Store)
HACS is the Docker user’s replacement for the add-on store. It gives you access to thousands of custom integrations and Lovelace dashboard themes — install once, then browse and one-click install forever.
docker compose exec homeassistant bash
wget -O - https://get.hacs.xyz | bash -
exit
docker compose restart homeassistant
After restart, go to Settings → Devices & Services → Add Integration → HACS. You will need a free GitHub account to authenticate. Once HACS is installed, use it to add integrations like Frigate (NVR), Adaptive Lighting, and Browser Mod.
Step 7 — Add Zigbee2MQTT Integration
After the Zigbee2MQTT container is running (check at http://<host-ip>:8080), enable the frontend, then in Home Assistant go to Settings → Devices & Services → Add Integration → MQTT. Home Assistant auto-discovers the Zigbee2MQTT bridge and all paired devices appear automatically.
To pair your first Zigbee device: set permit_join: true in zigbee2mqtt/data/configuration.yaml, restart the container (docker compose restart zigbee2mqtt), then put your device in pairing mode (usually hold a button for 5 seconds). The device appears in the Home Assistant dashboard within seconds. Disable permit_join immediately afterward.
Step 8 — Backup Strategy
Your entire configuration lives in ~/docker/homeassistant/. Back it up with:
# Stop containers to ensure consistent backups
cd ~/docker/homeassistant && docker compose stop
rsync -a ~/docker/homeassistant/ /mnt/nas/backups/homeassistant/$(date +%Y-%m-%d)/
docker compose up -d
Automate this with a cron job or a Proxmox Backup Server job (if your Docker host is a Proxmox VM/LXC). The Home Assistant container also has a built-in backup service accessible at Settings → System → Backups — use it to create snapshots that include add-on and integration data.
Reverse Proxy & Remote Access
Exposing Home Assistant directly on port 8123 to the internet is a bad idea. Use a reverse proxy — it gives you HTTPS, path-based routing, and access control in one place.
Option 1: Nginx Proxy Manager (Recommended)
If you already use Nginx Proxy Manager (and you should — see our NPM Docker Compose guide), adding Home Assistant is a 30-second task:
- In NPM, add a new Proxy Host.
- Domain:
home.example.com(or whatever subdomain you use). - Forward Hostname/IP:
<host-ip>and Port:8123. - Enable WebSocket Support (critical — Home Assistant uses WebSockets for real-time state updates).
- Request an SSL certificate via Let’s Encrypt.
That is it. Your Home Assistant is now accessible at https://home.example.com with automatic HTTPS renewal.
Option 2: Cloudflare Tunnel (No Port Forwarding)
If your ISP uses CGNAT or you want zero open ports, use Cloudflare Tunnel. See our Cloudflare Tunnel homelab guide for the full setup, but the Home Assistant-specific config looks like:
# In your cloudflared config.yml
tunnel: <tunnel-id>
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: home.example.com
service: http://<host-ip>:8123
- service: http_status:404
Cloudflare Tunnel wraps your Home Assistant in Cloudflare’s CDN and DDoS protection. Bonus: you get free analytics on who accesses your smart home (and from where).
Option 3: Tailscale Mesh VPN
For maximum security with zero exposed ports, install Tailscale on both your Docker host and your phone/laptop. Access Home Assistant at http://<tailscale-hostname>:8123 from anywhere. No reverse proxy, no DNS records, no TLS certificates needed. Combine this with Tailscale’s subnet routing to access your entire LAN remotely through one node.
Zigbee/Z-Wave USB Dongle Passthrough
This is the section that trips up most Docker users. Passing a USB device into a container is straightforward on bare metal and slightly more involved on Proxmox LXC.
Bare Metal / VM Host
Use the devices: block in docker-compose.yml as shown above. Find your dongle’s stable symlink:
ls -la /dev/serial/by-id/
# Output: usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_XXXXXXXX -> ../../ttyUSB0
Use the /dev/serial/by-id/... path in your Compose file — it survives USB port changes and reboots.
Permission fix: If Zigbee2MQTT logs show "Error: cannot open /dev/ttyUSB0":
sudo usermod -aG dialout $USER
# Log out and back in, or reboot
The privileged: true flag on the Home Assistant container usually handles this, but the Zigbee2MQTT container needs the dialout group access on the host.
Proxmox LXC USB Passthrough
In the Proxmox web UI:
1. Select your LXC container → Resources → Add → Device Passthrough.
2. Choose /dev/serial/by-id/usb-Silicon_Labs_... from the dropdown.
3. Restart the LXC.
Inside the LXC, the device appears at the same path. No container-level devices: block is needed — the Proxmox host-level passthrough supersedes it.
Common issue: If the device does not appear inside the LXC, check that the LXC is privileged (not recommended for security) or add the device’s cgroup permissions in /etc/pve/lxc/<container-id>.conf:
lxc.cgroup2.devices.allow: c 188:* rwm
lxc.mount.entry: /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_XXXXXXXX dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_XXXXXXXX none bind,optional,create=file
Updating & Maintenance
Docker Compose Update Workflow
Home Assistant releases a new version every month. Update your stack with:
cd ~/docker/homeassistant
# Pull new images (respects version pins in .env)
docker compose pull
# Recreate containers with new images
docker compose up -d
# Remove old dangling images
docker image prune -f
Version Pinning Strategy
The .env file pins major.minor.patch versions. Never use the latest tag — Home Assistant introduces breaking changes in minor releases (e.g., a Python version bump, an integration deprecation). Before bumping a version:
- Read the Home Assistant release notes.
- Check the Breaking Changes section.
- Review your active integrations for deprecation warnings.
- Bump the version in
.env, pull, and restart.
Portainer-Managed Updates
If you use Portainer (and you should — see our Portainer Docker management guide), you can manage the entire Home Assistant stack through the Portainer UI. Add the stack via Stacks → Add Stack → Web Editor, paste your Compose file, and deploy. Portainer shows container logs, resource usage, and update status — useful when you are on mobile and need to restart Home Assistant without SSH.
Troubleshooting Common Issues
Home Assistant Not Discovering Devices (mDNS/DLNA/UPnP)
Symptom: Google Cast, Sonos, Philips Hue, or Apple HomeKit devices do not appear in auto-discovery.
Fix: Verify network_mode: host is set on the homeassistant service. If you must use bridge mode for security reasons, you lose auto-discovery — add devices manually by IP address in the integration settings.
Zigbee2MQTT Cannot Access USB Dongle
Symptom: Logs show "Error: Error: cannot open /dev/ttyUSB0 (Permission denied)".
Fix: On the host: sudo usermod -aG dialout $USER && newgrp docker. Also verify the device path with ls /dev/serial/by-id/ — the symlink may have changed after a kernel update.
Container Not Starting After Reboot
Symptom: After a host reboot, docker compose ps shows containers as exited.
Fix: Ensure restart: unless-stopped is set on every service. If containers still do not auto-start, check that Docker itself starts on boot: sudo systemctl enable docker.
Database Corruption Recovery
Symptom: Home Assistant starts but history/logbook pages are blank.
Fix: The default SQLite database can corrupt after unexpected power loss. Purge it:
docker compose stop homeassistant
rm ~/docker/homeassistant/homeassistant/config/home-assistant_v2.db
docker compose start homeassistant
You lose history data (entity states, logbook entries) but your configuration (automations, scripts, dashboards) is unaffected — those live in separate YAML files. Consider switching to a MariaDB or PostgreSQL recorder for production setups.
FAQ: Docker Compose vs HAOS vs Core
Q: Can I use add-ons with Docker? No. Add-ons are exclusive to HAOS and Supervisor. In Docker, you run each add-on as a separate container in the same Compose stack. This is a lateral move: Mosquitto as a container works identically to the Mosquitto add-on; it just appears in Portainer instead of the Supervisor UI. For add-ons that do not have standalone Docker images, use HACS custom integrations.
Q: Is Docker less stable than HAOS?
No. Under the hood, HAOS runs the same homeassistant/home-assistant Docker image. The Supervisor is a separate container that manages add-ons and snapshots — it does not affect core stability. The stability difference is zero. What changes is who manages the companion services: you (Docker) or the Supervisor (HAOS).
Q: Should I run Home Assistant in a VM instead? If you want HAOS features (add-ons, one-click backups, Google Drive backup) without dedicating a physical machine, a Proxmox VM running HAOS is an excellent middle ground. You get the full HAOS experience while your other Docker containers run in separate VMs or LXCs on the same Proxmox host. The downside: you reserve RAM and CPU for the VM instead of sharing via containers.
Q: How do I back up Home Assistant in Docker?
Two methods: (1) rsync the entire ~/docker/homeassistant/ directory while containers are stopped — this captures everything including your Compose file, MQTT config, and Zigbee2MQTT data. (2) Use Home Assistant’s built-in backup tool at Settings → System → Backups to create a .tar snapshot, then copy it off-host with SCP or rsync. Either method works; method 1 is more complete because it captures non-HA data too.
Q: Can I run Home Assistant on a Raspberry Pi 5 with Docker? Yes, and it works well — with one caveat. Use an NVMe SSD (via a HAT or USB 3.0 enclosure), not a microSD card. Home Assistant’s recorder writes to the database continuously; SD cards wear out in 6–12 months under that load. With an SSD, a Raspberry Pi 5 (8 GB) handles a modest smart home of 20–30 Zigbee devices and a few Wi-Fi integrations without breaking a sweat.
Conclusion: Your Smart Home, Your Rules
Running Home Assistant with Docker Compose gives you the best of both worlds: the power and flexibility of the world’s best open-source smart home platform, plus the infrastructure consistency of your existing homelab stack. You manage Home Assistant the same way you manage Jellyfin, Pihole, and Nginx Proxy Manager — one Compose file, one update workflow, one backup strategy.
The tradeoff — no add-on store — is smaller than it appears. HACS fills the integration gap, and running companion services as separate containers gives you finer control over versions, resource limits, and networking. For a homelabber already comfortable with Docker, it is the natural choice.
Next steps from here:
- Set up Nginx Proxy Manager to expose Home Assistant securely with HTTPS
- Deploy Cloudflare Tunnel for remote access without opening ports
- Choose the best NAS OS for storing Home Assistant backups, media, and camera footage
- Monitor your entire homelab with Grafana and Prometheus — track CPU, RAM, and disk usage of your Home Assistant host
- Run Ollama on Proxmox to add local AI-powered voice assistants to your smart home
Happy automating — and remember: permit_join: false.