👍 What we like
- ✓Officially supported Container method, using the multi-architecture ghcr.io/home-assistant/home-assistant:stable image.
- ✓The whole stack (Home Assistant, MQTT, Zigbee) lives in one folder that is easy to back up and restore.
- ✓Zigbee2MQTT and the TP-Link integration work locally, without the cloud.
- ✓Controlled updates with docker compose pull, after reading the release notes.
👎 What to watch
- ✕No add-on store as in Home Assistant OS: each extra service is a container you manage yourself.
- ✕Requires an always-on 64-bit machine; 32-bit systems are no longer supported.
- ✕Requires some knowledge of Docker, MQTT and Linux permissions.
- ✕Recent HS100 firmware requires your TP-Link credentials for local access.
🏆 Our picks
Affiliate links · same price for youTP-Link HS100 Smart Plug Mini
Amazon · price and availability on the listing
Western Digital WD My Cloud EX2 Ultra NAS
Amazon · price and availability on the listing
See all our recommended gear in the shop.
📑 Contents ▾
- 01 Prerequisites
- 02 Step-by-step installation
- · Step 1: Install Docker Engine and the Compose plugin
- · Step 2: Identify the Zigbee dongle
- · Step 3: Create the directory tree and configure Mosquitto
- · Step 4: Write the compose.yaml file
- · Step 5: Start the stack
- · Step 6: First Home Assistant launch
- · Step 7: Configure Zigbee2MQTT (onboarding)
- · Step 8: Add the MQTT integration in Home Assistant
- · Step 9: Pair a Zigbee device, add the HS100 and create an automation
- · Step 10 (optional): Local HTTPS with Caddy
- 13 Verification checks
- 14 Security and best practices
- 15 Troubleshooting common errors
- 16 FAQ
- · Should I use ZHA or Zigbee2MQTT?
- · Is the Raspberry Pi 4 Model B enough?
- · Can I install Home Assistant on the WD My Cloud EX2 Ultra?
- · How do I update Home Assistant via Docker?
- 21 Read also
- 22 Host this yourself
There are several ways to install Home Assistant. This guide covers the Home Assistant Container method: Home Assistant run with Docker Compose on a Linux system you manage yourself. It is one of the two installation methods still officially supported, along with Home Assistant OS. The “Core” and “Supervised” methods are deprecated, and 32-bit systems (armv7, armhf, i386) have not been supported since release 2025.12. You will install, in a single Docker Compose stack:
- Home Assistant, using the official
ghcr.io/home-assistant/home-assistant:stableimage; - Mosquitto, the MQTT broker;
- Zigbee2MQTT, which drives your Zigbee USB dongle and passes your sensors to Home Assistant over MQTT;
- optionally, Caddy, for HTTPS access on the local network.
The Container edition doesn’t have the add-on store that comes with Home Assistant OS. That’s why Mosquitto and Zigbee2MQTT run in their own containers here.
Prerequisites
- A 64-bit Linux machine (amd64 or aarch64) that stays on all the time. A Raspberry Pi 4 Model B works if it runs a 64-bit system, such as Raspberry Pi OS 64-bit or Ubuntu Server 64-bit. For reliability, use a USB SSD rather than the microSD card.
- Docker Engine 23.0 or newer with the Compose plugin (
docker compose). The Home Assistant documentation says Docker Desktop is not supported. - A Zigbee USB dongle that Zigbee2MQTT supports. Note which chip it uses (Texas Instruments, Silicon Labs…): you will need it in step 7.
sudoaccess and a fixed IP address (or a DHCP reservation) for the server.- Optional: a TP-Link HS100 Smart Plug Mini. It is a Wi-Fi plug, not a Zigbee one. Home Assistant controls it locally through the “TP-Link Smart Home” integration.
About the Western Digital WD My Cloud EX2 Ultra: this NAS has a 32-bit ARM processor and its system does not provide standard Docker. It is therefore not suitable for hosting Home Assistant. It can, however, store copies of your backups on your local network.
Step-by-step installation
Step 1: Install Docker Engine and the Compose plugin
The commands below come from Docker’s Ubuntu documentation (22.04, 24.04 and 26.04 LTS are supported). On Raspberry Pi OS 64-bit or Debian, follow the “Debian” page of the Docker documentation instead. First uninstall any unofficial packages that are present (docker.io, docker-compose, docker-compose-v2, podman-docker).
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Check:
sudo docker run --rm hello-world
sudo docker version --format '{{.Server.Version}}'
sudo docker compose version
hello-world should print its confirmation message. The server version should be 23.0 or higher, and docker compose version should respond without an error.
Step 2: Identify the Zigbee dongle
Plug in the dongle, then look up its stable path. The Zigbee2MQTT documentation recommends always using /dev/serial/by-id/…, because /dev/ttyUSB0 or /dev/ttyACM0 can change from one reboot to the next.
ls -l /dev/serial/by-id/
You should get a line like usb-ITead_Sonoff_Zigbee_3.0_USB_Dongle_Plus_…-if00-port0 -> ../../ttyUSB0. Copy the full name. If the folder doesn’t exist, the system isn’t detecting the dongle: check with sudo dmesg | tail -n 20 right after plugging it in.
Step 3: Create the directory tree and configure Mosquitto
The whole stack lives in one folder, which makes backups simpler.
mkdir -p ~/homeassistant/{homeassistant/config,mosquitto/config,mosquitto/data,mosquitto/log,zigbee2mqtt/data,caddy/data,caddy/config}
cd ~/homeassistant
tee mosquitto/config/mosquitto.conf <<'EOF'
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
EOF
Mosquitto 2.x rejects anonymous clients as soon as a listener is declared, so you need to create accounts. Use one account per client: homeassistant and zigbee2mqtt. The -c option creates the file; use it only for the first account, or the file gets overwritten.
sudo docker run --rm -it -v "$PWD/mosquitto/config:/mosquitto/config" eclipse-mosquitto \
mosquitto_passwd -c /mosquitto/config/passwd homeassistant
sudo docker run --rm -it -v "$PWD/mosquitto/config:/mosquitto/config" eclipse-mosquitto \
mosquitto_passwd /mosquitto/config/passwd zigbee2mqtt
sudo docker run --rm eclipse-mosquitto id mosquitto
The last command prints the UID and GID of the image’s mosquitto user. Give that user the file and restrict its permissions (replace 1883:1883 if the command prints something else):
sudo chown 1883:1883 mosquitto/config/passwd
sudo chmod 0700 mosquitto/config/passwd
Check: sudo cat mosquitto/config/passwd shows two lines, homeassistant:$7$… and zigbee2mqtt:$7$…. The passwords in it are hashed.
Step 4: Write the compose.yaml file
Create ~/homeassistant/compose.yaml. The homeassistant service follows the official example: network_mode: host so network discovery works (including for the HS100 plug), and privileged: true. The dongle is passed only to Zigbee2MQTT. Replace REMPLACEZ-PAR-VOTRE-DONGLE with the name you noted in step 2.
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
volumes:
- ./homeassistant/config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro
restart: unless-stopped
stop_grace_period: 60s
privileged: true
network_mode: host
environment:
TZ: Europe/Paris
depends_on:
- mosquitto
mosquitto:
container_name: mosquitto
image: eclipse-mosquitto
restart: unless-stopped
ports:
- "127.0.0.1:1883:1883"
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
zigbee2mqtt:
container_name: zigbee2mqtt
image: ghcr.io/koenkk/zigbee2mqtt:latest
restart: unless-stopped
depends_on:
- mosquitto
ports:
- "8080:8080"
volumes:
- ./zigbee2mqtt/data:/app/data
- /run/udev:/run/udev:ro
environment:
TZ: Europe/Paris
devices:
- /dev/serial/by-id/REMPLACEZ-PAR-VOTRE-DONGLE:/dev/ttyACM0
caddy:
container_name: caddy
image: caddy:2
restart: unless-stopped
network_mode: host
profiles: ["proxy"]
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
This file makes three choices:
- Mosquitto listens only on
127.0.0.1on the host. Home Assistant, inhostmode, reaches it at127.0.0.1:1883, and Zigbee2MQTT reaches it over Compose’s internal network under the namemosquitto. - The
/run/dbusfolder is only used by the Bluetooth integration. You can remove that line if you don’t use Bluetooth. - Caddy sits in the
proxyprofile, so it only starts when you ask for it (step 10).
Check:
sudo docker compose config --quiet && echo "compose.yaml valide"
If the command reports an error (indentation, unknown key…), it gives the offending line.
Step 5: Start the stack
sudo docker compose up -d
sudo docker compose ps
Check: all three services, homeassistant, mosquitto and zigbee2mqtt, are running (or Up). Then test the web server and the broker:
curl -sI http://127.0.0.1:8123 | head -n 1
sudo tail -n 5 mosquitto/log/mosquitto.log
The first command should return an HTTP/1.1 line with a 200 status or a redirect (3xx). The first start can take a minute or two: run the command again if it fails right away. The Mosquitto log should mention the listener opening on port 1883.
Step 6: First Home Assistant launch
From a computer on the local network, open http://SERVER_IP:8123. Create the owner account, then set your location, time zone and units. If you use UFW, allow port 8123 from your local network only (adjust the range):
sudo ufw allow from 192.168.1.0/24 to any port 8123 proto tcp
Check: once the wizard is done, the dashboard appears, and sudo ls homeassistant/config lists at least configuration.yaml and automations.yaml.
Step 7: Configure Zigbee2MQTT (onboarding)
On first start, Zigbee2MQTT runs an initial setup page on port 8080. Open http://SERVER_IP:8080 and fill in:
- MQTT server:
mqtt://mosquitto:1883, with thezigbee2mqttuser and its password; - Serial port:
/dev/ttyACM0(the path inside the container); - Adapter type: this depends on the chip, for example
zstackfor Texas Instruments oremberfor Silicon Labs EFR32 (check the Zigbee2MQTT documentation for your dongle); - enable the Home Assistant integration (MQTT discovery).
Once you confirm the onboarding, Zigbee2MQTT starts. Check:
sudo cat zigbee2mqtt/data/configuration.yaml
sudo docker compose logs --tail 30 zigbee2mqtt
Among other things, the file should contain the equivalent of this:
mqtt:
base_topic: zigbee2mqtt
server: mqtt://mosquitto:1883
user: zigbee2mqtt
serial:
port: /dev/ttyACM0
homeassistant:
enabled: true
frontend:
enabled: true
The logs should contain neither Not authorized nor ECONNREFUSED, and should show that the connection to the MQTT server succeeded.
Step 8: Add the MQTT integration in Home Assistant
Since release 2022.3, the broker connection is no longer configured in YAML. Go to Settings > Devices & services > Add integration, search for MQTT, then enter:
- Broker:
127.0.0.1 - Port:
1883 - Username:
homeassistant, with its password
Check: in Settings > Devices & services > MQTT > Configure, under “Listen to a topic”, enter zigbee2mqtt/bridge/state and click “Start listening”. You should receive a message containing online. You can also run this from the server:
sudo docker compose exec mosquitto mosquitto_sub -h localhost -u homeassistant -P 'YOUR_PASSWORD' -t 'zigbee2mqtt/bridge/state' -C 1 -v
Step 9: Pair a Zigbee device, add the HS100 and create an automation
- In the Zigbee2MQTT interface (port 8080), click Permit join, then put your sensor into pairing mode (see its manual). It appears in the device list, and Home Assistant creates it automatically under Settings > Devices & services > MQTT.
- For the TP-Link HS100 plug: Settings > Devices & services. It often shows up under “Discovered”; if not, go to Add integration > TP-Link Smart Home and enter its IP address. Recent firmware asks for your TP-Link account credentials. If you get an authentication error, enable Third-Party Compatibility in the Kasa app (Settings > Third-Party Compatibility). After that, communication stays local, without the cloud.
- Create the automation in Settings > Automations & scenes > Create automation. In the ⋮ menu, choose “Edit in YAML” and paste the following, replacing the entity IDs with your own (you can find them in Settings > Devices & services > Entities):
alias: "Allumer la prise quand le couloir détecte un mouvement"
description: "Capteur Zigbee via Zigbee2MQTT -> prise TP-Link HS100"
triggers:
- trigger: state
entity_id: binary_sensor.capteur_couloir_occupancy
to: "on"
actions:
- action: switch.turn_on
target:
entity_id: switch.prise_salon
mode: single
Check: trigger the sensor. The plug should turn on, and the automation’s Trace (⋮ menu > Traces) shows the trigger and the action that ran.
Step 10 (optional): Local HTTPS with Caddy
The goal is HTTPS access on the local network (or through a VPN), without opening any port on your router. Since you can’t get a public certificate without exposing the server, this uses Caddy’s internal certificate authority (tls internal). The name ha.home.arpa is an example: add it to your router’s local DNS (or to the hosts file of your client machines) so it points to the server’s IP.
tee caddy/Caddyfile <<'EOF'
ha.home.arpa {
tls internal
reverse_proxy 127.0.0.1:8123
}
EOF
sudo docker compose --profile proxy up -d caddy
sudo docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
Next, tell Home Assistant to trust the proxy. Otherwise, requests coming from the reverse proxy are blocked (the browser typically shows a 400 error). Since release 2026.8, this is set in Settings > System > Network, HTTP server section: enable Trust X-Forwarded-For and add 127.0.0.1 and ::1 to Trusted proxies. On an older release, add this block to homeassistant/config/configuration.yaml, then restart with sudo docker compose restart homeassistant:
http:
use_x_forwarded_for: true
trusted_proxies:
- 127.0.0.1
- ::1
Check:
curl -kI --resolve ha.home.arpa:443:127.0.0.1 https://ha.home.arpa | head -n 1
The status should be 200 or a redirect, not 400. To stop browsers from showing a warning, import Caddy’s root certificate, caddy/data/caddy/pki/authorities/local/root.crt, on your machines.
Verification checks
cd ~/homeassistant
sudo docker compose ps
sudo docker compose logs --tail 50 homeassistant | grep -iE "error|exception" || echo "aucune erreur récente"
curl -sI http://127.0.0.1:8123 | head -n 1
Also check in the interface:
- Settings > System > Repairs shows no blocking alert;
- under Settings > Devices & services, the MQTT and TP-Link Smart Home integrations are active, with no errors.
Security and best practices
- Never expose Home Assistant (8123), Zigbee2MQTT (8080), Mosquitto (1883) or SSH directly to the Internet. For remote access, use a VPN (WireGuard, for example). Don’t create any port forwarding on your router.
- According to the Docker documentation, ports published by Docker (here, Zigbee2MQTT’s 8080) bypass UFW rules. If you want to restrict access to it, publish it on a specific IP (
"192.168.1.10:8080:8080") or add rules to theDOCKER-USERchain. - Enable two-factor authentication in your Home Assistant Profile > Security, and keep a separate MQTT account for each client.
- Updates: read the release notes (the “Backward-incompatible changes” section), back up, then run:
cd ~/homeassistant
sudo docker compose stop homeassistant zigbee2mqtt
sudo tar czf ~/sauvegarde-ha-$(date +%F).tar.gz homeassistant/config zigbee2mqtt/data mosquitto/config
sudo docker compose pull
sudo docker compose up -d
- To stay in control of updates, you can replace
stablewith a specific version tag (see the tags published on Home Assistant’s GitHub package page). Then copy the archive to another device, for example a NAS share on your local network.
Troubleshooting common errors
docker: 'compose' is not a docker command: the plugin is missing. Installdocker-compose-plugin(step 1). The olddocker-compose(v1) binary is no longer maintained.- Zigbee2MQTT won’t start, with
no such file or directoryon the device: the/dev/serial/by-id/…path is wrong or the dongle is unplugged. Runls -l /dev/serial/by-id/again and fixcompose.yaml. - Zigbee2MQTT logs
Not authorized: the MQTT username or password is wrong, or thepasswdfile doesn’t exist. Check step 3 and themosquitto/log/mosquitto.loglog. - Zigbee2MQTT logs
ECONNREFUSED: Mosquitto is stopped or the server setting is wrong. It must bemqtt://mosquitto:1883, notlocalhost, becauselocalhostrefers to the Zigbee2MQTT container itself. - Port 8123 doesn’t respond from the local network: the firewall is blocking it (see the UFW rule in step 6) or Home Assistant hasn’t finished starting (
sudo docker compose logs -f homeassistant). - 400 error through Caddy: trusted proxies aren’t configured (step 10). The Home Assistant log reports a request from an unauthorized reverse proxy.
- The HS100 isn’t discovered: it’s on another subnet or VLAN. Add it by its (fixed) IP, or enable Third-Party Compatibility in the Kasa app.
FAQ
Should I use ZHA or Zigbee2MQTT?
Both work, but never at the same time on the same dongle. ZHA is built into Home Assistant: you then pass the dongle to the homeassistant service (devices: block) and remove Zigbee2MQTT. Zigbee2MQTT runs separately, has its own interface and goes through MQTT, as in this guide.
Is the Raspberry Pi 4 Model B enough?
Yes, for home use, provided you run a 64-bit system: 32-bit images are no longer supported. For longevity, use a USB SSD rather than the microSD card.
Can I install Home Assistant on the WD My Cloud EX2 Ultra?
No. Home Assistant no longer supports its 32-bit ARM processor, and its system doesn’t offer standard Docker. Use it to store copies of your backup archives on the local network.
How do I update Home Assistant via Docker?
Run sudo docker compose pull and then sudo docker compose up -d, after reading the release notes and making a backup (see “Security and best practices”). Compose only recreates the containers whose image has changed.
Read also
Host this yourself
No hardware to manage? This kind of service runs on a managed VPS, ready in minutes. See PlanetHoster plans