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:
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.1network_mode: host
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 1privileged: true
entries instead for a narrower surface.1devices:
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 1./ha-config:/config
it, back it up with rsync, or snapshot it before upgrades.1git init
Bring it up:
1docker compose up -d 2
Then browse to
. The onboarding wizard walks you through creating your admin account and detecting devices — it's genuinely good.1http://<host-ip>:8123
Config file layout
After onboarding,
will contain:1ha-config/
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
starts nearly empty. Over time you add integrations and customizations here. For example, to enable the history graph and set your home coordinates:1configuration.yaml
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 (
), not in YAML. I only drop to YAML for things the UI doesn't expose.1Settings → Devices & Services → Add Integration
A real automation: lights off when everyone leaves
Automations live in
and have a dead-simple structure: a trigger fires the automation, optional conditions gate it, and actions do the work.1automations.yaml
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
is a person group I created under1group.all_people
. When everyone transitions to1Settings → People
, every light turns off. No cron, no script, no cloud subscription.1not_home
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
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.1for:
Keeping config in Git
Because everything under
is plain files, version control is trivial:1ha-config/
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
), 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.1devices:
That's the thing about Home Assistant. The initial setup takes an afternoon. Then you keep finding one more thing to automate.
