Home Assistant on Raspberry Pi 4 with Docker and MQTT Auto-Discovery
Home Assistant is open-source home automation software that brings lights, plugs, sensors, cameras and thousands of other devices into one dashboard with automations that run locally, without depending on a cloud service. The Raspberry Pi 4 with 4 GB or 8 GB of RAM is one of the most common platforms for it.
This advanced tutorial runs Home Assistant and the Mosquitto MQTT broker as containers with Docker Compose, connects them, and then writes a Python script that publishes the Pi's own sensor data using MQTT discovery, so the entities appear in Home Assistant automatically without editing any YAML in Home Assistant.
The Docker Compose Stack
Create a folder ~/homeassistant containing the compose file below and a mosquitto/config/mosquitto.conf file.
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant:stable
container_name: homeassistant
volumes:
- ./ha-config:/config # all HA settings and the database
- /etc/localtime:/etc/localtime:ro # use the Pi's timezone
- /run/dbus:/run/dbus:ro # needed for Bluetooth integrations
network_mode: host # required for device discovery (mDNS, SSDP)
privileged: true # access to USB radios and hardware
restart: unless-stopped
mosquitto:
image: eclipse-mosquitto:2
container_name: mosquitto
ports:
- "1883:1883" # MQTT
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
restart: unless-stopped
# Listen on all interfaces on the standard MQTT port
listener 1883
# Mosquitto 2.x refuses anonymous clients unless told otherwise; keep it that way
allow_anonymous false
password_file /mosquitto/config/passwd
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
cd ~/homeassistant
# Create the broker password file with a user called 'mqttuser'
docker run --rm -it -v $PWD/mosquitto/config:/mosquitto/config eclipse-mosquitto:2 \
mosquitto_passwd -c /mosquitto/config/passwd mqttuser
docker compose up -d
docker compose logs -f homeassistant # wait until startup finishesOpen http://pi4lab.local:8123, create the owner account, and set your location. Then go to Settings, Devices and services, Add integration, choose MQTT, and enter broker localhost (Home Assistant uses host networking), port 1883, and the mqttuser credentials.
How MQTT Discovery Works
Home Assistant listens on topics beginning with homeassistant/. When a device publishes a retained JSON message to homeassistant/<component>/<object_id>/config, Home Assistant creates the entity described in it: its name, unit, device class and the state topic where values will arrive. Because the config message is retained, the broker replays it whenever Home Assistant restarts.
| Topic | Example payload | Purpose |
|---|---|---|
| homeassistant/sensor/pi4lab_cpu_temp/config | JSON entity description | Creates the entity |
| pi4lab/state | {"cpu_temp": 52.1, "load1": 0.4} | Carries the readings |
| pi4lab/availability | online or offline | Marks the device unavailable if it disconnects |
Publishing Pi Sensors with Discovery
Install the client library with sudo apt install -y python3-paho-mqtt and run this script on the Pi, or on any other Pi on the network by changing BROKER.
#!/usr/bin/env python3
# pi_mqtt_sensors.py - expose Raspberry Pi metrics to Home Assistant over MQTT
import json
import os
import socket
import time
import paho.mqtt.client as mqtt
BROKER, PORT = 'localhost', 1883
USER, PASSWORD = 'mqttuser', 'change-me' # the mosquitto_passwd user
HOST = socket.gethostname() # e.g. pi4lab
STATE_TOPIC = f'{HOST}/state'
AVAIL_TOPIC = f'{HOST}/availability'
# Device block groups all entities under one device in Home Assistant
DEVICE = {'identifiers': [HOST], 'name': f'{HOST} Raspberry Pi 4',
'manufacturer': 'Raspberry Pi Ltd', 'model': 'Raspberry Pi 4 Model B'}
SENSORS = {
'cpu_temp': {'name': 'CPU temperature', 'unit_of_measurement': '\u00b0C',
'device_class': 'temperature', 'state_class': 'measurement'},
'load1': {'name': 'Load average 1 min', 'state_class': 'measurement'},
}
def read_values():
with open('/sys/class/thermal/thermal_zone0/temp') as f:
temp = int(f.read()) / 1000
return {'cpu_temp': round(temp, 1), 'load1': round(os.getloadavg()[0], 2)}
# paho-mqtt 2.x needs a callback API version; 1.x does not have this attribute
try:
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
except AttributeError:
client = mqtt.Client()
client.username_pw_set(USER, PASSWORD)
# Last will: broker publishes 'offline' if this script dies unexpectedly
client.will_set(AVAIL_TOPIC, 'offline', retain=True)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start() # network loop in a background thread
# Publish one retained discovery config per sensor
for key, extra in SENSORS.items():
config = {
'unique_id': f'{HOST}_{key}',
'state_topic': STATE_TOPIC,
'value_template': '{{ value_json.' + key + ' }}',
'availability_topic': AVAIL_TOPIC,
'device': DEVICE,
**extra,
}
client.publish(f'homeassistant/sensor/{HOST}_{key}/config',
json.dumps(config), retain=True)
client.publish(AVAIL_TOPIC, 'online', retain=True)
try:
while True:
values = read_values()
client.publish(STATE_TOPIC, json.dumps(values))
print(values)
time.sleep(30)
except KeyboardInterrupt:
client.publish(AVAIL_TOPIC, 'offline', retain=True)
finally:
client.loop_stop()
client.disconnect()
Within a few seconds a new device named after your Pi appears under the MQTT integration with two sensors. Add them to a dashboard card, or use the temperature in an automation, for example sending a notification when it exceeds 75 °C.
Key Points in the Script
- unique_id lets Home Assistant track the entity across restarts and lets you rename it or change its icon from the user interface.
- value_template picks one field out of the shared JSON state message, so a single publish updates every sensor at once.
- will_set registers a last-will message with the broker. If the Pi loses power or the network drops, the broker publishes 'offline' on its behalf after the keepalive period expires.
- retain=True on the config and availability messages means Home Assistant receives them even if it starts after the script.
- state_class: measurement tells Home Assistant to keep long-term statistics, so the temperature appears in history graphs with hourly averages.
To keep the publisher running permanently, create a systemd service for it in the same way as the Gunicorn service in the web server article, with Restart=on-failure and After=network-online.target.
Troubleshooting
- Not authorised in the script: the username or password does not match the file created by
mosquitto_passwd. Recreate it and restart the mosquitto container. - No entities appear: confirm the config topics with
mosquitto_sub -h localhost -u mqttuser -P <password> -t 'homeassistant/#' -vinside the container. - Entities show Unavailable: the availability topic says offline. Check that the script is running and that the retained 'online' message was sent.
- Home Assistant slow or database errors: the recorder writes constantly. Run the stack from a USB SSD rather than a microSD card.
- Zigbee USB stick not found: pass the device through, or rely on
privileged: true, and use the stable path under/dev/serial/by-id/.
Summary
Home Assistant Container and Mosquitto run side by side in one compose file, with host networking for Home Assistant and a password-protected broker. Publishing retained discovery messages under homeassistant/sensor/.../config lets any Python script on any Pi create fully described entities automatically, with a last-will message marking them unavailable if the publisher stops. This is the foundation for bringing your own GPIO and sensor projects into a real home automation system.