Smart Home Privacy

HA Container + Docker Compose Add-Ons Locally 2026

Home Assistant Container without Supervisor: Docker Compose patterns for MQTT, Zigbee2MQTT, and AdGuard Home-style DNS—privacy-first networking, USB radios, backups, and hardening.

Privacy Smart Home Research Desk May 04, 2026

Keywords: home assistant container add-ons, home assistant docker compose, zigbee2mqtt docker home assistant, mosquitto docker privacy, local dns smart home adguard

Quick answer: How do I replace Home Assistant add-ons when I run Container?

Model each add-on as its own service in Docker Compose: Home Assistant talks to Mosquitto, Zigbee2MQTT, and local DNS (for example AdGuard Home) over a private Docker network. Mount `/config`, pass USB radios with `devices`, pin image tags, and block unnecessary WAN egress from IoT VLANs.

Source: Home Assistant installation — Container


Executive Summary

If you chose Home Assistant Container, you traded the Supervisor’s one-click add-ons for explicit infrastructure. That is often the right trade for privacy: you decide which containers exist, which ports are exposed, and which subnets can reach the public internet. This guide walks through a practical Docker Compose layout for the three services people miss most after leaving Home Assistant OS: an MQTT broker, a Zigbee coordinator path via Zigbee2MQTT, and local DNS filtering that behaves like an AdGuard Home add-on without requiring Home Assistant to own those packages.

You are not “missing features” so much as moving them one layer down to the orchestrator that already runs your home lab. The comparison article on HA OS vs Container vs Supervised explains why this path suits advanced users; here we focus on repeatable wiring, safe defaults, and failure modes that affect real homes (radio paths, mDNS, backups, and accidental cloud dependency).

Bottom line: Treat Compose as your supervisor: one git-backed compose.yaml, named volumes, a single internal bridge network, explicit published ports only where needed, and a firewall posture that keeps cameras and voice assistants from “phoning home” when you intended local control.


Why Container users plan around Compose (not magic add-ons)

On Home Assistant OS, add-ons wrap commonly paired services so beginners avoid YAML for adjacent containers. On Container, Home Assistant is just another container image; anything else—MQTT brokers, Zigbee gateways, DNS, Frigate—is your responsibility to define, version, and upgrade. That sounds like extra work, and it is—but it is also the clearest way to reason about data flows: Zigbee radios touch Zigbee2MQTT, MQTT clients talk to Mosquitto, Home Assistant subscribes to topics, and DNS resolvers decide whether a cheap Wi‑Fi plug can resolve *.tuya.com over TLS.

Most stability issues we see in community reports are not “Home Assistant bugs” but integration boundaries: wrong network_mode, devices moved between USB paths after kernel updates, or a broker restarted without persistence because ./data was never mounted as a volume1. Compose forces those boundaries into a file you can diff, review, and restore—similar in spirit to infrastructure-as-code for a single host.

DecisionHome Assistant OS + add-onsHome Assistant Container + Compose
Who defines extra services?Supervisor manifestsYou, in Compose
Upgrade cadenceOften coupled to HA releasesIndependent per image tag
USB radio accessAbstractedExplicit devices: mapping
Rollback storyBackup/restore focusedPin tags + volume snapshots
Privacy leversStill good; less transparentMaximum transparency

If you are new to Container installs, read the official Linux container instructions and confirm you are not confusing Core (Python venv) with the supervised deprecation story—they overlap in community threads but mean different support expectations1.


Network layout: bridge networks, published ports, and DNS loops

A resilient default is: one custom bridge network (for example ha_internal) shared by Home Assistant, Mosquitto, Zigbee2MQTT, and AdGuard Home. Use service names as hostnames (mqtt, zigbee2mqtt, homeassistant) so containers can reach each other without exposing broker ports on 0.0.0.0 unless you truly need LAN clients.

Many users accidentally create DNS loops when AdGuard listens on the same Docker host Home Assistant queries for upstream resolution. A simple pattern is: AdGuard listens on a LAN or VLAN interface; Home Assistant container sets dns: in Compose to that resolver only after you verify forwarders (unbound, your ISP, or DoT upstreams). Until then, point Home Assistant at a known-good upstream during bootstrap, then switch to local filtering.

PatternWhen it helpsPitfall
Internal bridge onlyAll MQTT clients are containersLAN devices cannot publish without extra exposure
Publish MQTT to LANESPHome devices on Wi‑FiWeak ACLs on broker
Host network for Zigbee2MQTTOdd USB + mDNS edge casesHarder to firewall; port clashes
MACVLAN for Zigbee2MQTTIsolated L2 appearanceMore ops burden

Cross-read MQTT broker comparison when choosing Mosquitto vs EMQX; Mosquitto remains the default pairing in Z2M docs and many compose examples, but EMQX can simplify clustering if you outgrow a single broker2.


Compose skeleton: Home Assistant + Mosquitto + Zigbee2MQTT + AdGuard Home

The following is a pattern, not a copy-paste for every distribution: adapt paths, user namespace options (PUID/PGID), and device nodes. Always pin image tags in production (image: ghcr.io/home-assistant/home-assistant:2026.x.y style) instead of :latest.

You will usually mount:

  • Home Assistant: /config to a host path or named volume
  • Mosquitto: mosquitto.conf, passwd, persistence directories
  • Zigbee2MQTT: /app/data with configuration.yaml pointing at mqtt://mqtt:1883
  • AdGuard Home: its work + conf directories per upstream docs

Environment knobs worth documenting in your repo:

  • TZ consistent across stacks (automation timestamps and log correlation)
  • ZIGBEE2MQTT_CONFIG path if you prefer file injection
  • Home Assistant homeassistant.external_url / internal_url when you later add reverse proxies—see Caddy vs Traefik vs NPM before exposing anything.

Operational habits matter as much as YAML:

  1. docker compose pull && docker compose up -d on a schedule you can sustain
  2. Watch for SELinux/AppArmor profiles on NAS distributions blocking USB access
  3. Snapshot volumes before Zigbee2MQTT major upgrades (network rebuild pain is real)
  4. Keep Zigbee coordinator firmware reasonably current—but read release notes; some updates change channel plans
ServiceTypical internal hostnamePort(s) to LAN
Home Assistanthomeassistant8123 via reverse proxy or LAN only
Mosquittomqttoptional 1883 if LAN MQTT clients exist
Zigbee2MQTTzigbee2mqttoptional 8080 for UI—restrict by firewall
AdGuard Homeadguard53 UDP/TCP on LAN/VLAN interfaces

Annotated compose.yaml pattern (Home Assistant + Mosquitto + Zigbee2MQTT)

Below is a teaching skeleton you can adapt. Paths are placeholders (/srv/ha/...); replace with your NAS share, BTRFS subvolume, or ZFS dataset. Comments inline explain why each stanza matters for privacy and recoverability.

name: homeassistant-stack
services:
  homeassistant:
    container_name: homeassistant
    image: ghcr.io/home-assistant/home-assistant:stable # pin to 2026.x.y in real deployments
    volumes:
      - /srv/ha/homeassistant:/config
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped
    networks: [ha_internal]
    depends_on: [mqtt]
    # Uncomment after AdGuard is stable on your LAN; avoid resolver loops during first boot
    # dns:
    #   - 10.0.20.2
    environment:
      - TZ=Etc/UTC

  mqtt:
    container_name: mosquitto
    image: eclipse-mosquitto:2
    restart: unless-stopped
    networks: [ha_internal]
    ports:
      # Publish 1883 to the LAN only if Wi-Fi MQTT clients need it; otherwise omit ports:
      # - "192.168.10.5:1883:1883"
      - "127.0.0.1:1883:1883"
    volumes:
      - /srv/ha/mosquitto/config:/mosquitto/config
      - /srv/ha/mosquitto/data:/mosquitto/data
      - /srv/ha/mosquitto/log:/mosquitto/log

  zigbee2mqtt:
    container_name: zigbee2mqtt
    image: koenkk/zigbee2mqtt:latest # pin a release tag when you trust the stack
    restart: unless-stopped
    networks: [ha_internal]
    depends_on: [mqtt]
    volumes:
      - /srv/ha/z2m:/app/data
    devices:
      # Prefer by-id symlinks so USB re-enumeration does not break pairing
      - /dev/serial/by-id/usb-ITEAD_CHIP12345-if00-port0:/dev/ttyUSB0
    environment:
      - TZ=Etc/UTC
    ports:
      # Expose UI only on loopback if you SSH-tunnel, or bind to management VLAN IP
      - "127.0.0.1:8080:8080"

  adguardhome:
    container_name: adguardhome
    image: adguard/adguardhome:latest
    restart: unless-stopped
    network_mode: host # common for DNS; see AdGuard docs for Docker caveats on your distro
    volumes:
      - /srv/ha/caddy/adguard/work:/opt/adguardhome/work
      - /srv/ha/caddy/adguard/conf:/opt/adguardhome/conf

networks:
  ha_internal:
    driver: bridge

What this layout encodes:

  • Home Assistant waits on MQTT (depends_on) so logs during boot are less noisy—not a guarantee Mosquitto finished loading ACLs, but a practical ordering hint.
  • Broker port bound to loopback in the example keeps ESPHome off-LAN MQTT from working until you deliberately publish to a trusted interface. When you are ready, bind to your IoT VLAN gateway IP instead of 0.0.0.0.
  • Zigbee2MQTT UI on localhost reduces attack surface; pair Zigbee from a machine that can reach that port, or add HTTP auth + reverse proxy later.
  • AdGuard network_mode: host is common because DNS listeners are awkward behind Docker NAT. If you dislike host mode, study macvlan/ipvlan patterns—but budget time; DNS is not a five-minute side quest.

After docker compose up -d, validate from the host:

  1. docker compose logs -f mqtt—look for clean listener startup and ACL parse errors
  2. docker compose exec mosquitto mosquitto_sub ...—verify anonymous publish is disabled if you enabled passwords
  3. Home Assistant → Settings → Devices—ensure MQTT integration points at mqtt://mqtt:1883 inside the stack (service name DNS)
SymptomFirst checks
HA cannot reach brokerSame Docker network? Typo in service name? Firewall on host blocking bridge?
Zigbee2MQTT starts, no trafficRadio permissions, permit_join, channel conflict with Wi-Fi
DNS works on laptop, not HAHA dns: still pointing at WAN while AdGuard filters upstream
Disk fills overnightretained MQTT messages + aggressive debug logging

Operational upgrades, secrets, and Git hygiene

Compose users routinely leak .env files into public GitHub repos. Keep secret material in a password manager and reference it with env_file: .secrets/mqtt.env that is git-ignored. For Home Assistant long-lived tokens, prefer the UI-backed secrets or secrets.yaml patterns documented upstream—never paste API keys into Compose files you email to yourself.

Upgrade discipline:

  • Read Mosquitto, Zigbee2MQTT, and Home Assistant release notes in that order when Zigbee is involved—Z2M occasionally bumps defaults that require configuration.yaml tweaks
  • Take filesystem snapshots before Zigbee2MQTT bumps that mention database migrations
  • For Home Assistant, watch breaking changes for MQTT discovery prefixes—cheap Tuya derivatives sometimes ship hard-coded topics

If you integrate Frigate or other NVR software later, isolate their heavy CPU spikes from Zigbee coordination by CPU affinity or separate bare-metal hosts; Zigbee is sensitive to USB noise and scheduling latency on oversubscribed NAS CPUs.


USB radios: Linux device paths, firmware, and coordinator choice

Zigbee2MQTT needs stable access to /dev/ttyUSB* or /dev/serial/by-id/.... Prefer by-id symlinks in Compose devices: so reboots do not silently renumber ports. If you stack multiple UART bridges, label the physical stick and verify with udevadm.

Coordinator selection is a site decision, not something this guide ranks as “best.” What matters for privacy workflows is: local pairing, support in Zigbee2MQTT, and whether you can run multi-network setups if you segment test hardware. For integration semantics inside Home Assistant, skim Zigbee2MQTT vs ZHA vs deCONZ before you commit—migrating Zigbee networks later is costly.


Backups, observability, and “why did my house stop?”

Container installs shine when you treat config + state as precious artifacts:

  • Home Assistant: snapshot /config; test restores quarterly
  • Zigbee2MQTT: backup configuration.yaml and the coordinator database files Z2M maintains; losing them can mean re-pairing dozens of devices
  • Mosquitto: persist retained messages only when you understand disk growth; apply ACLs if you multi-tenant
  • AdGuard Home: export settings after meaningful DNS filter changes; keep an emergency upstream if you filter too aggressively and break NTP

Logging: bind container logs to your homelab standard (Loki, plain journald, or simple log rotation). The goal is to notice MQTT disconnect storms or DNS filter regressions before family members do.


Privacy hardening beyond “it runs locally”

Running on Docker does not automatically stop devices from exfiltrating. Pair this stack with VLAN policies and resolver choices discussed in blocking IoT DNS leaks and broader network stacks such as private network stack.

Concrete checklist items:

  • Deny IoT VLAN direct WAN except to the explicit NTP/DNS path you intend
  • Use local DNS with blocklists cautiously—some hubs break if you block telemetry domains they require for handshakes (log and allowlist deliberately)
  • Prefer TLS to MQTT when clients support it; many ESPHome nodes remain plaintext on LAN—compensate with segmentation
  • Review Zigbee2MQTT permit_join and disable it after pairing windows

Checklist

  • Pin all container image tags and document upgrade steps.
  • Mount persistent volumes for every stateful service (broker, Z2M, AdGuard).
  • Map Zigbee radios by /dev/serial/by-id and regression-test after kernel updates.
  • Restrict Zigbee2MQTT and Mosquitto admin interfaces to management networks.
  • Snapshot configs before Zigbee or major Home Assistant upgrades.
  • Verify DNS forwarding avoids resolver loops between HA and AdGuard.
  • Block or allowlist IoT egress deliberately—do not rely on “local API” alone.
Consulting-style infographic mapping Home Assistant Container with Mosquitto MQTT, Zigbee2MQTT, and local DNS services on Docker Compose with privacy-focused network boundaries.
Compose is your supervisor: explicit services, explicit trust boundaries.

FAQ

Frequently Asked Questions

Is Home Assistant Container officially supported?

Yes. Container is a first-class installation method; you simply do not get the Supervisor UI or add-on store. Expect to self-manage adjacent services through Docker or systemd1.

Can I replicate every Home Assistant add-on with Docker?

Practically, most add-ons are upstream images with opinionated wrappers. Find the upstream project (for example Eclipse Mosquitto or Zigbee2MQTT) and configure it yourself. Some proprietary bridges have no perfect drop-in—plan substitutes.

Should Zigbee2MQTT use host networking?

Only if you hit a specific compatibility issue. Start with bridge networking and explicit ports; move to host mode when documentation or maintainers document a concrete need. Host mode complicates firewalling.

How do I avoid losing my Zigbee network on upgrades?

Back up Zigbee2MQTT data files before upgrades, read release notes for database migrations, and upgrade coordinators firmware only when instructed. Re-pairing dozens of routers and end devices is the expensive failure mode you are avoiding.

Do I need AdGuard Home if I already use Pi-hole?

No. Both are DNS sinkholes with different UX and feature sets. Pick one resolver stack and integrate Home Assistant with consistent dns: settings in Compose—avoid running multiple competing LAN DNS servers without intent.


Primary Sources Table

IDSourceDirect URL
1Home Assistant docs — Container on Linuxhttps://www.home-assistant.io/installation/linux#install-home-assistant-container
2Zigbee2MQTT Docker guidehttps://www.zigbee2mqtt.io/guide/installation/02_docker.html
3Eclipse Mosquitto documentationhttps://mosquitto.org/documentation/
4Docker Compose specificationhttps://docs.docker.com/compose/
5AdGuard Home — Docker installationhttps://hub.docker.com/r/adguard/adguardhome

Conclusion

Home Assistant Container is not a downgrade—it is an architecture where you own every hop. Compose gives you reproducible stacks, clearer privacy boundaries, and the freedom to upgrade Mosquitto on Tuesday and Home Assistant on Sunday. Invest in backups, stable Zigbee device maps, and DNS topology early; those three save more weekend hours than any automation gimmick.

For adjacent decisions, continue with Mosquitto vs EMQX, Zigbee stack comparison, and the installation tradeoffs in HA OS vs Container vs Supervised.

Footnotes

  1. Home Assistant documentation — installation methods and Container notes. 2 3

  2. Zigbee2MQTT project documentation — Docker deployment and MQTT connectivity patterns.