Running Home Assistant in Docker: From Zero to Your First Automation

Running Home Assistant in Docker: From Zero to Your First Automation

Home Assistant is the kind of project that sounds like a weekend toy until you've lived with it for a month. Now it controls my lights, monitors my NAS disk temps, sends me alerts when a door is left open, and does a dozen other things I've completely stopped thinking about. The best part: it runs on a $35 Raspberry Pi (or a VM, or a plain Docker container), and every bit of the config is plain YAML I keep in version control.

In this post I'll show you the exact Docker Compose setup I use, explain the config file layout, and walk through two real automations so you can see how the pieces fit together.

Why Docker Container mode

Home Assistant ships in four flavors: Home Assistant OS (full VM image), Supervised (OS-managed), Container (a single Docker image), and Core (bare Python). I use Container mode because it integrates cleanly into my existing Docker Compose homelab — same tooling, same restart policies, same backup approach as everything else I run.

The trade-off is that add-ons (HA's curated app store) don't work in Container mode. In practice I've never needed them: anything an add-on provides I can spin up as its own service in the same Compose file.

The Compose file

1services:
2  homeassistant:
3    image: ghcr.io/home-assistant/home-assistant:stable
4    container_name: homeassistant
5    restart: unless-stopped
6    network_mode: host
7    environment:
8      - TZ=America/New_York
9    volumes:
10      - ./ha-config:/config
11    privileged: true
12

A few things worth calling out:

1network_mode: host
is the single most important line. Home Assistant uses mDNS and SSDP to discover devices on your local network. With bridge networking, those multicast packets never reach the container. Host mode sidesteps the problem entirely.

1privileged: true
is only required if you're passing through USB devices (Zigbee sticks, Z-Wave adapters, etc.). If you're purely using Wi-Fi or cloud integrations you can drop it and use
1devices:
entries instead for a narrower surface.

1./ha-config:/config
mounts a local directory as the config root. This is where all YAML files, the SQLite database, and logs live. Keeping it on the host means I can
1git init
it, back it up with rsync, or snapshot it before upgrades.

Bring it up:

1docker compose up -d
2

Then browse to

1http://<host-ip>:8123
. The onboarding wizard walks you through creating your admin account and detecting devices — it's genuinely good.

Config file layout

After onboarding,

1ha-config/
will contain:

1ha-config/
2├── configuration.yaml   # main entry point
3├── automations.yaml     # automations list (HA manages this)
4├── scripts.yaml         # scripts
5├── scenes.yaml          # scenes
6├── .storage/            # entity registry, device registry, UI state
7└── home-assistant.log
8

1configuration.yaml
starts nearly empty. Over time you add integrations and customizations here. For example, to enable the history graph and set your home coordinates:

1homeassistant:
2  name: Home
3  latitude: 40.7128
4  longitude: -74.0060
5  unit_system: imperial
6  time_zone: America/New_York
7
8history:
9
10recorder:
11  purge_keep_days: 30
12

Most integrations are configured through the UI these days (

1SettingsDevices & ServicesAdd Integration
), not in YAML. I only drop to YAML for things the UI doesn't expose.

A real automation: lights off when everyone leaves

Automations live in

1automations.yaml
and have a dead-simple structure: a trigger fires the automation, optional conditions gate it, and actions do the work.

1- id: "lights_off_on_away"
2  alias: "Turn off all lights when everyone leaves"
3  trigger:
4    - platform: state
5      entity_id: group.all_people
6      to: "not_home"
7  action:
8    - service: light.turn_off
9      target:
10        area_id: all
11

1group.all_people
is a person group I created under
1SettingsPeople
. When everyone transitions to
1not_home
, every light turns off. No cron, no script, no cloud subscription.

A real automation: alert if a door is left open

This one uses a time trigger and a state condition together:

1- id: "front_door_left_open"
2  alias: "Alert if front door open for 5 minutes"
3  trigger:
4    - platform: state
5      entity_id: binary_sensor.front_door_contact
6      to: "on"
7      for:
8        minutes: 5
9  action:
10    - service: notify.mobile_app_my_phone
11      data:
12        title: "Door Alert"
13        message: "Front door has been open for 5 minutes."
14

The

1for:
key on a state trigger is the cleanest way to add a debounce — the trigger only fires if the door has been open continuously for five minutes. No timer entity needed, no extra condition.

Keeping config in Git

Because everything under

1ha-config/
is plain files, version control is trivial:

1cd ha-config
2git init
3echo ".storage/" >> .gitignore   # internal HA state, not useful to track
4echo "home-assistant.log" >> .gitignore
5echo "*.db" >> .gitignore
6git add .
7git commit -m "initial HA config"
8

Now I can diff every automation change, roll back a bad edit, and see exactly when I introduced a regression. Upgrades become low-stakes: tag the current commit, pull the new image, bring it up, and if anything breaks I still have a clean rollback path.

What's next

I've been slowly wiring in more sensors: a Zigbee motion sensor in the garage (via a Sonoff Zigbee 3.0 USB dongle passed through with

1devices:
), a template sensor that derives "is anyone watching TV" from the TV's power draw, and a few REST sensors polling my NAS API. Each one follows the same pattern — add the integration, watch the entity appear in the UI, write a simple automation against it.

That's the thing about Home Assistant. The initial setup takes an afternoon. Then you keep finding one more thing to automate.