Design goals
Deployment
How this installation is deployed and why — the design decisions that matter when you are standing up (or improving) your own Home Assistant instance.
Design goals
- Docker, not the Supervisor image. A plain container means the whole installation is described by a compose file plus a config directory. No Supervisor snapshot system to depend on, and the config directory can simply be a git checkout — which makes configuration itself version-controlled and diffable.
- Config in git. The config directory on the host is a git clone of a
dedicated
homeassistant-configrepository. Every change is a commit, and — when running a cluster — every instance pulls the same repository, which is what makes active/standby failover possible (see High Availability). - PostgreSQL as the recorder backend. The default SQLite file is fine for a small setup, but once you have many entities and several days of history a real database is noticeably faster. Because the database is co-located on the same host, it is reached over a unix socket, not TCP.
- Each instance reachable independently. Every server runs its own nginx
reverse proxy with a dynamic subdomain setup: one TLS server block catches
any subdomain of the domain and maps the subdomain to a backend port. Each
Home Assistant instance therefore gets its own subdomain and its own unique
external_url, so the two instances can be told apart from the outside even while the watchdog decides which one is “the” Home Assistant.
Postgres over a unix socket
The host’s /var/run/postgresql directory is bind-mounted into the container
(as /postgresql), and the recorder DSN points at the socket
(postgresql://user:pass@/hass?host=/postgresql...).
Why a socket when HA and the database live on the same machine:
- Efficient — no TCP/IP stack: no port binding, no packet framing, no loopback overhead, no Nagle/delayed-ACK behaviour. Lower latency and less CPU per query, which matters for the recorder’s frequent small writes.
- Simpler to wire up — no port to expose, no firewall rules, no
listen_addresses/pg_hba.conftuning for a TCP endpoint, and nothing to collide with on the host. - Tight security by default — the socket lives on the filesystem, so access is governed by file permissions rather than being exposed to the network.
- No DNS/host resolution — the client connects to a path, avoiding any hostname or IP drift between the container and the database.
The one constraint: the host’s PostgreSQL unix_socket_directories and the
container’s auth settings must line up — the standard, well-trodden
local-Postgres setup.
Reverse proxy (nginx)
The pattern in use is a dynamic subdomain reverse proxy: one map block
converts the captured subdomain into a backend port, a single HTTP server
redirects everything to HTTPS, and one master HTTPS server proxies to
127.0.0.1:$backend_port with WebSocket headers (needed for HA, ESPHome,
etc.). Each subdomain also gets its own access log, which makes per-service
diagnostics trivial.
map $dynamic_subdomain $backend_port {
esphome 6052;
grafana 3000;
hass 8123; # one subdomain per HA instance
ha2 8123; # same port — different subdomain, same local service
# add new ones here as you spin them up
}
server {
listen 80;
listen [::]:80;
server_name ~^(?<dynamic_subdomain>.+)\.<tld>$;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name ~^(?<dynamic_subdomain>.+)\.<tld>$;
http2 on;
access_log /var/log/nginx/$dynamic_subdomain-access.log;
include snippets/ssl-letsencrypt.conf;
location / {
proxy_pass http://127.0.0.1:$backend_port;
include proxy_params; # centralised Host / X-Forwarded-* headers
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
}
}
Notes:
- HA’s HTTP settings (trusted proxies, forwarded headers) are configured in the
UI and rely on the
proxy_paramsheaders above. - The proxy must run on the same machine as the service (backends are
addressed at
127.0.0.1). - TLS is via Let’s Encrypt (
snippets/ssl-letsencrypt.conf).
Secrets
Everything that varies per instance or that must not be committed lives in a
local secrets.yaml (git-ignored) and is referenced from the shared config
with !secret <name>. In this installation the per-instance secrets are the
instance name, internal_url, external_url, the PostgreSQL DSN, and
heartbeat_url (the partner instance’s webhook URL — see
High Availability).
Never commit
secrets.yaml,service_account.json, or SSH keys. Keep them in.gitignore.
docker compose
The complete compose file (each instance in the cluster runs the same file on
its own server — only the config directory’s local secrets.yaml and the
Postgres backend differ):
services:
homeassistant:
container_name: home-assistant
image: ghcr.io/home-assistant/home-assistant:stable
volumes:
- /var/local/docker-files/home-assistant:/config
- /Media:/Media
- /var/run/postgresql:/postgresql
- /run/dbus:/run/dbus:ro
environment:
- TZ=Australia/Perth
restart: always
# ports:
# - 8123:8123
network_mode: host
privileged: true
Notes:
network_mode: host— no published ports (the commented-out8123:8123is unnecessary); the container uses the host’s network stack directly, which is also how the per-instance nginx proxy and the cluster webhook pats reach it./var/local/docker-files/home-assistantis the git checkout of thehomeassistant-configrepository — the config directory is the version control system for the configuration./var/run/postgresql:/postgresql— the Postgres unix socket mount (see Postgres over a unix socket)./run/dbus(read-only) — host D-Bus access, e.g. for UPnP/mDNS features./Media:/Media— media directory mount.privileged: true— worth revisiting if it can be narrowed to specific device/capability grants.
Related
Source Disclaimer
- AI Generated
- Human Generated
- AI Edited
- Human Edited