Add Home Assistant (home box) + ha-proxy (Hetzner/Dokploy) stacks
- home-assistant/: HA Core + matter-server, host networking (home box, not Dokploy) - ha-proxy/: nginx reverse-proxy, ha.manohargupta.com -> home HA over Tailscale - dual-homed (dokploy-network ingress + bridge egress), mirrors n8n pattern
This commit is contained in:
parent
d868499b42
commit
4fc7893d28
5 changed files with 195 additions and 0 deletions
50
ha-proxy/docker-compose.yml
Normal file
50
ha-proxy/docker-compose.yml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# ============================================================================
|
||||
# ha-proxy -- ha.manohargupta.com (runs on HETZNER via DOKPLOY)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Home Assistant runs at HOME. This stack is ONLY a reverse-proxy bridge:
|
||||
#
|
||||
# Browser --TLS--> Traefik (Hetzner) --> ha-proxy (nginx) --tailnet--> HA @ home
|
||||
#
|
||||
# WHY nginx and not a plain Traefik route to the home IP:
|
||||
# - Traefik (Dokploy's) sits on the dokploy-network swarm OVERLAY, which can't
|
||||
# cleanly egress to a tailnet peer. nginx here is dual-homed: it takes
|
||||
# ingress from Traefik on dokploy-network, and egresses to the home box
|
||||
# over a local BRIDGE network (ha_egress) whose gateway is the Hetzner host
|
||||
# -- the host then routes to tailscale0. This is the same dokploy-network +
|
||||
# bridge pattern your n8n stack already uses successfully.
|
||||
# - nginx also handles the WebSocket upgrade HA's frontend depends on.
|
||||
#
|
||||
# RAM cost on Hetzner: ~10-15 MB (nginx:alpine). The heavy part stays home.
|
||||
# ============================================================================
|
||||
services:
|
||||
ha-proxy:
|
||||
image: nginx:1.27-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
networks:
|
||||
- dokploy-network # ingress: Traefik routes ha.manohargupta.com here
|
||||
- ha_egress # egress: container -> host -> tailscale0 -> home box
|
||||
# --- Container-level labels (docker provider) ---
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=dokploy-network"
|
||||
- "traefik.http.routers.ha.rule=Host(`ha.manohargupta.com`)"
|
||||
- "traefik.http.routers.ha.entrypoints=websecure"
|
||||
- "traefik.http.routers.ha.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.ha.loadbalancer.server.port=80"
|
||||
# --- Service-level labels (swarm provider) -- Dokploy deploys as swarm stack ---
|
||||
deploy:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=dokploy-network"
|
||||
- "traefik.http.routers.ha.rule=Host(`ha.manohargupta.com`)"
|
||||
- "traefik.http.routers.ha.entrypoints=websecure"
|
||||
- "traefik.http.routers.ha.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.ha.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
ha_egress:
|
||||
driver: bridge
|
||||
47
ha-proxy/nginx.conf
Normal file
47
ha-proxy/nginx.conf
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# nginx.conf -- ha-proxy (Hetzner, behind Traefik)
|
||||
# Forwards ha.manohargupta.com -> Home Assistant on the home box over Tailscale.
|
||||
# Traefik terminates TLS; this listens plain HTTP on :80 inside the network.
|
||||
|
||||
worker_processes 1;
|
||||
events { worker_connections 256; }
|
||||
|
||||
http {
|
||||
# --- WebSocket upgrade plumbing -------------------------------------
|
||||
# HA's frontend uses a persistent WebSocket (/api/websocket). Without this
|
||||
# map the UI loads then hangs "Connecting...". The map sets the Connection
|
||||
# header to "upgrade" only when the client requested an upgrade.
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# !!! EDIT THIS: the home box's TAILSCALE IP (100.x.y.z), port 8123 !!!
|
||||
# Find it after the home box joins your tailnet: `tailscale ip -4` on that box.
|
||||
upstream homeassistant {
|
||||
server 100.XX.XX.XX:8123;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name ha.manohargupta.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://homeassistant;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# WebSocket upgrade
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Preserve host + client info so HA's trusted_proxies check passes
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# HA long-lived connections: don't cut them off early
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
home-assistant/README.md
Normal file
34
home-assistant/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Home Assistant — ha.manohargupta.com
|
||||
|
||||
HA runs **at home**; Hetzner only proxies the subdomain. Two deploy targets:
|
||||
|
||||
- `home-assistant/` → HOME BOX (Pi/N100), plain `docker compose up -d`. NOT Dokploy.
|
||||
- `ha-proxy/` → HETZNER via Dokploy (nginx reverse-proxy over Tailscale).
|
||||
|
||||
## Deploy order
|
||||
1. **Home box**: `docker compose up -d` → onboard HA at `http://<lan-ip>:8123` →
|
||||
paste `configuration.snippet.yaml` into `config/configuration.yaml` → restart.
|
||||
2. Join home box to Tailscale; `tailscale ip -4` → note the 100.x IP.
|
||||
`ufw allow in on tailscale0 to any port 8123` on the home box.
|
||||
3. **DNS**: `ha` A-record → `77.42.82.225` (done).
|
||||
4. **Hetzner**: set the home tailnet IP in `ha-proxy/nginx.conf`, deploy `ha-proxy`
|
||||
as a Dokploy Compose app.
|
||||
|
||||
## Dead-man's-switch — verify BEFORE trusting the cert flow
|
||||
From inside the running ha-proxy container:
|
||||
```
|
||||
wget -qO- http://100.XX.XX.XX:8123 | head -c 200
|
||||
```
|
||||
HTML back → good. Hang/refused → host isn't forwarding container→tailnet; check
|
||||
`ip route get 100.XX.XX.XX` resolves via tailscale0 + Docker bridge MASQUERADE.
|
||||
|
||||
## Why this shape
|
||||
- HA needs `network_mode: host` (mDNS/Matter multicast + LAN RTSP for Tapo).
|
||||
- An HA instance in Helsinki cannot reach home-LAN cameras — hence home box.
|
||||
- ha-proxy is dual-homed (dokploy-network ingress + bridge egress), same pattern
|
||||
as the n8n stack, because the swarm overlay can't egress to a tailnet peer.
|
||||
|
||||
## Security (flagged)
|
||||
Public subdomain = HA login is internet-facing. Enable HA 2FA; consider Authentik
|
||||
forward-auth later. Tailnet-only access (skip the public route) is safer if you
|
||||
only reach HA from your own devices.
|
||||
18
home-assistant/configuration.snippet.yaml
Normal file
18
home-assistant/configuration.snippet.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# configuration.yaml SNIPPET -- paste into ./config/configuration.yaml on the HOME box
|
||||
# ----------------------------------------------------------------------------
|
||||
# WHY this is required:
|
||||
# HA refuses proxied requests by default (anti-spoofing). When ha-proxy forwards
|
||||
# from Hetzner over the tailnet, the request reaches HA with a SOURCE IP equal to
|
||||
# the Hetzner node's tailnet IP (after the host masquerades it out tailscale0).
|
||||
# You must whitelist that IP, or every page load fails with HTTP 400
|
||||
# "received from untrusted proxy / IP address not allowed".
|
||||
#
|
||||
# If you hit a 400, check the HA log -- it prints the exact rejected IP. Put THAT
|
||||
# IP in trusted_proxies (it should be 100.75.128.45, your Hetzner tailnet IP).
|
||||
|
||||
http:
|
||||
use_x_forwarded_for: true
|
||||
trusted_proxies:
|
||||
- 100.75.128.45 # Hetzner (manohar-ubuntu) tailnet IP = the proxy's source
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
46
home-assistant/docker-compose.yml
Normal file
46
home-assistant/docker-compose.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# ============================================================================
|
||||
# Home Assistant + Matter Server -- runs on the HOME BOX (Pi 5 / N100)
|
||||
# ----------------------------------------------------------------------------
|
||||
# THIS DOES NOT RUN ON HETZNER / DOKPLOY.
|
||||
# It lives in the infra repo for source-control + documentation, but it is
|
||||
# deployed by hand on the home machine: `docker compose up -d`
|
||||
#
|
||||
# WHY host networking (network_mode: host) is mandatory here:
|
||||
# - Tapo cameras are reached over your LAN (RTSP 554 / ONVIF 2020). HA must
|
||||
# sit on the same L2 segment to discover + stream them.
|
||||
# - mDNS / SSDP / Matter commissioning are MULTICAST. Bridge networking
|
||||
# drops multicast at the container boundary, so discovery silently fails.
|
||||
# With host mode the container shares the home box's network stack directly.
|
||||
# ============================================================================
|
||||
services:
|
||||
homeassistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:stable
|
||||
container_name: homeassistant
|
||||
restart: unless-stopped
|
||||
network_mode: host # see header note -- non-negotiable for HA
|
||||
volumes:
|
||||
- ./config:/config # all HA state + configuration.yaml lives here
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /run/dbus:/run/dbus:ro # lets HA see host Bluetooth/dbus (harmless if unused)
|
||||
environment:
|
||||
- TZ=Asia/Kolkata
|
||||
# NOTE: no `ports:` needed -- host mode already exposes :8123 on the box.
|
||||
# NOTE: no `privileged` -- only required if you later pass a USB Zigbee/Thread
|
||||
# dongle, which would also need a `devices:` mapping. Not needed for
|
||||
# WiFi / RTSP / Matter-over-WiFi devices.
|
||||
|
||||
# --- Matter controller (you mentioned Matter devices) -------------------
|
||||
# HA talks to this over ws://localhost:5580 (add the "Matter" integration in
|
||||
# the HA UI and point it there). Comment this whole block out if you want to
|
||||
# hold off on Matter for now -- Tapo cameras do NOT need it.
|
||||
matter-server:
|
||||
image: ghcr.io/home-assistant-libs/python-matter-server:stable
|
||||
container_name: matter-server
|
||||
restart: unless-stopped
|
||||
network_mode: host # Matter commissioning needs multicast too
|
||||
security_opt:
|
||||
- apparmor=unconfined # required for the Matter stack's raw network access
|
||||
volumes:
|
||||
- ./matter-data:/data
|
||||
environment:
|
||||
- TZ=Asia/Kolkata
|
||||
Loading…
Add table
Reference in a new issue