You almost never need a Home Assistant Dockerfile — the official image is complete, and what people actually want is a docker-compose.yml that mounts a config volume and sets the right network mode.
Searching for a Dockerfile usually means one of two questions: how do I run this in a container, or how do I add a system package the base image lacks. The first is answered by Compose. The second is the only genuine reason to write a Dockerfile here, and it comes with an upgrade cost worth understanding before you commit.
Table of contents
- The Compose file, which is what you are looking for
- network_mode: host, and what it costs you
- Volumes, and the one that decides whether you lose everything
- USB devices for Zigbee and Z-Wave
- When a Dockerfile is genuinely the answer
- Updating without regret
- How this fits the rest of the stack
- FAQ
The Compose file, which is what you are looking for
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
volumes:
- ./config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro # only if you need Bluetooth
restart: unless-stopped
privileged: true
network_mode: host
Create the directory and start it:
mkdir -p config
docker compose up -d
docker compose logs -f
First boot takes a couple of minutes while it generates its configuration. Then it is on port 8123.
That file is the whole deployment. Everything worth arguing about is in four of those lines, so it is worth going through them.
network_mode: host, and what it costs you
This is the setting people skip and then spend an evening on.
Home Assistant discovers devices using mDNS, SSDP, and DHCP broadcasts. Those are broadcast protocols, and broadcasts do not cross Docker’s default bridge network. In bridge mode the web interface works fine and auto-discovery finds nothing — which reads like a broken integration rather than a networking choice.
network_mode: host puts the container directly on the host’s network stack, so discovery behaves as it would running natively.
What you give up is real: the container shares the host’s ports, so ports: mappings are ignored and any port conflict with the host is now your problem. There is no network isolation from the host at all. On a dedicated device that is an acceptable trade. On a shared server running other services, think about it first.
If you genuinely need bridge mode, map the port explicitly and accept that you will be adding integrations by IP address rather than having them found:
# bridge alternative -- discovery will not work
ports:
- "8123:8123"
Volumes, and the one that decides whether you lose everything
./config:/config is the only mount that matters for durability. Everything Home Assistant knows — your entities, automations, history database, integration credentials, dashboards — lives in /config. Without that bind mount, all of it is inside the container’s writable layer, and the next docker compose pull deletes it.
That is not a hypothetical. It is the single most common way people lose a Home Assistant setup: months of automations gone during a routine update, because the container was started without a volume and everything looked fine right up until it did not.
Back it up on a schedule. It is a directory, so this is not complicated:
tar czf "ha-config-$(date +%F).tar.gz" ./config
/etc/localtime:ro gives the container the host’s timezone. Without it the container runs UTC, and every time-based automation fires at the wrong hour — which presents as “my lights come on at the wrong time” rather than as a timezone problem.
The dbus mount is only needed for Bluetooth. Leave it out unless you are using it.
USB devices for Zigbee and Z-Wave
A Zigbee or Z-Wave stick has to be passed into the container, and the naive way breaks on reboot.
devices:
- /dev/serial/by-id/usb-Silicon_Labs_slae.sh_cc2652rb_stick-if00-port0:/dev/ttyUSB0
Use the /dev/serial/by-id/ path, never /dev/ttyUSB0 directly. The numbered device names are assigned in enumeration order, so plugging in a second USB device — or simply rebooting — can swap ttyUSB0 and ttyUSB1. Home Assistant then talks to the wrong radio and your entire Zigbee network appears to vanish.
The by-id path is derived from the device’s own identifiers and is stable across reboots. Find yours with:
ls -l /dev/serial/by-id/
With devices: declared you can usually drop privileged: true. Keep privileged only if something specifically needs it — it is a large permission to grant by default, and most setups do not require it.
When a Dockerfile is genuinely the answer
One case: you need a system package inside the container that the base image does not ship. A custom integration requiring a specific binary, a driver library, a CLI tool an automation shells out to.
FROM ghcr.io/home-assistant/home-assistant:stable
RUN apk add --no-cache nmap iputils
# Python packages a custom component needs at import time
RUN pip install --no-cache-dir some-vendor-sdk
The base image is Alpine, so it is apk add, not apt-get install. That catches people who copy instructions written for the Debian-based images.
services:
homeassistant:
build: .
container_name: homeassistant
volumes:
- ./config:/config
network_mode: host
restart: unless-stopped
The cost: you now own the upgrade. docker compose pull no longer fetches a new Home Assistant — it fetches nothing, because you build locally. Every release means rebuilding, and if a new base image changes something your extra packages depend on, you find out at build time instead of never.
docker compose build --pull
docker compose up -d
Before writing one, check whether the thing you need can be a separate container instead. A companion service on the same Compose file, talking to Home Assistant over its API, keeps the official image pristine and updatable. That is the better shape whenever it is possible.
Updating without regret
docker compose pull
docker compose up -d
docker image prune -f
Two habits worth having. Pin to a version rather than stable if you care about when upgrades happen — image: ghcr.io/home-assistant/home-assistant:2026.8 means an update is a deliberate edit rather than whatever pull fetched today.
And back up ./config before every update, not after. Home Assistant migrates its configuration database on major version bumps, and those migrations are not reversible. Rolling back the image without rolling back the config gets you a version that cannot read its own database.
The general principle applies well beyond this container: a stateful service is only as recoverable as the last copy of its state you took. The image is replaceable, the config directory is not.
How this fits the rest of the stack
Self-hosting on hardware you own trades a monthly bill for your own time and a single point of failure. When a project outgrows that — a service that needs to stay up, a database that needs backups, a route on the public internet — the RunxBuild hosting calculator shows what those line items add up to before you commit.
Useful related references:
- Ubuntu Home Server: Install, Static IP, SSH, and the First 5 Services
- Dockerfile COPY: The Instruction Order That Decides Your Build Speed
- Dockerfile Secrets: The Three Patterns That Actually Keep Them Out of the Image
- Docker services on RunxBuild
FAQ
Do I need a Dockerfile for Home Assistant?
Usually not. The official image at ghcr.io/home-assistant/home-assistant is complete, and a docker-compose.yml with a config volume and the right network mode covers almost every setup. A Dockerfile is only worth it when you need extra system packages inside the container.
Why is Home Assistant not discovering my devices in Docker?
Discovery uses mDNS, SSDP, and DHCP broadcasts, and broadcasts do not cross Docker’s bridge network. Set network_mode: host so the container shares the host network stack. The web interface works fine in bridge mode, which is why this looks like a broken integration rather than a networking choice.
Which volume does Home Assistant actually need?
./config:/config. Every entity, automation, credential, and the history database live there. Without the bind mount they sit in the container’s writable layer and are destroyed by the next image pull — the most common way people lose a Home Assistant setup.
How do I pass a Zigbee or Z-Wave USB stick into the container?
Use a devices: entry with the /dev/serial/by-id/ path rather than /dev/ttyUSB0. Numbered device names are assigned in enumeration order and can change on reboot or when another USB device is added, which makes the radio disappear. The by-id path is stable.
What is the downside of building a custom Home Assistant image?
You take over updates. docker compose pull no longer fetches new Home Assistant releases because you build locally, so every release means a rebuild with —pull. Before writing a Dockerfile, check whether the functionality can live in a companion container talking over the API instead.