---
title: "Deployment"
description: "## Design goals"
section: home-assistant
raw: "01-deployment.md"
source: ai-generated
tags: home-assistant, home-automation
last-updated: 2026-09-15
---

# 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-config` repository. 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](/home-assistant/02-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.conf` tuning 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.

```nginx
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_params` headers 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](/home-assistant/02-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):

```yaml
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-out `8123:8123`
  is 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-assistant` is the git checkout of the
  `homeassistant-config` repository — 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](#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

- [High Availability](/home-assistant/02-high-availability/)
- [Light brightness as state](/home-assistant/03-light-brightness-state/)

---

## Source Disclaimer

- [x] AI Generated
- [ ] Human Generated
- [x] AI Edited
- [ ] Human Edited
