Home Assistant in Docker Host Mode with Nginx Proxy and Let’s Encrypt the 2026 way
Or: how we turned “almost working” into “clean HTTPS and zero drama”
There are projects you start with dangerous confidence.
You think: Home Assistant in Docker, host networking, reverse proxy in front, Let’s Encrypt on top. Easy win.
And then three hours later you are staring at a hanging curl, your coffee is cold, and Nginx is silently judging your life choices.
Good news: this setup works beautifully once it is wired the right way.
In this post I’ll walk through the final, working solution for running Home Assistant in Docker host mode behind nginx-proxy with acme-companion handling the certificates for:
lin-ha.vanudengroup.com
This is the cleaned-up version. No half-working experiments, no “should work in theory,” and no mystery meat networking.
The environment
This build runs on an Ubuntu host with this IP:
192.168.1.166
The requirements were simple:
- Home Assistant in Docker
- Home Assistant in host network mode
- Nginx as a reverse proxy
- Automatic Let’s Encrypt certificates
- Public URL:
https://xxx.xxx.com
Nice and tidy.
The one big takeaway from the whole adventure was this:
If Home Assistant runs in host mode, it is often much cleaner to run the reverse proxy in host mode too.
That ended up being the magic ingredient.
Why host mode for Home Assistant?
Home Assistant is one of those applications that tends to be happiest in network_mode: host.
That gives you a few practical benefits:
- easier local network discovery
- less pain with mDNS, SSDP, and multicast-based integrations
- fewer awkward “why can’t this thing find my device?” moments
So that part stayed exactly as requested.
Where things went sideways
The first design looked like this:
- Home Assistant in host mode
nginx-proxyin bridge modeacme-companionalongside it- Nginx forwarding traffic to Home Assistant
On paper, perfectly reasonable.
In practice, we hit the classic issue:
- Home Assistant was listening correctly on port
8123 - Nginx was receiving requests
- Let’s Encrypt certificates were issued successfully
- but traffic over HTTPS would hang when Nginx tried to pass it upstream
That meant the outside looked polished, but internally the proxy path was broken.
Testing quickly showed the real story:
curl http://127.0.0.1:8123worked on the hostcurlfrom inside the Nginx container to the host-side Home Assistant service did not- so the failing path was container → host-mode Home Assistant
The clean fix was gloriously boring:
Run
nginx-proxyin host mode too, and proxy directly to127.0.0.1:8123.
No weird bridge routing.
No host-gateway guesswork.
No Docker networking séance.
Just straight traffic, like the networking gods intended.
The final working architecture
We are using:
nginxproxy/nginx-proxynginxproxy/acme-companionghcr.io/home-assistant/home-assistant:stable
And the final design is:
- Home Assistant runs in host mode
- nginx-proxy runs in host mode
- acme-companion handles Let’s Encrypt
- Nginx proxies to
127.0.0.1:8123 - Home Assistant trusts the proxy via
trusted_proxies
Simple. Stable. Pleasantly boring.
Step 1 – Create the folder structure
Create the working directory and the required subfolders:
mkdir -p /docker/ha-proxy/nginx/vhost.d
mkdir -p /docker/ha-proxy/homeassistant/config/themes
cd /docker/ha-proxy
Now create the empty include files Home Assistant expects if you use the standard includes:
touch /docker/ha-proxy/homeassistant/config/automations.yaml
touch /docker/ha-proxy/homeassistant/config/scripts.yaml
touch /docker/ha-proxy/homeassistant/config/scenes.yaml
If you skip those while referencing them in configuration.yaml, Home Assistant will go into recovery mode faster than a switch stack during a surprise firmware mismatch.
Step 2 – Create the .env file
File path:
/docker/ha-proxy/.env
Contents:
TZ=Europe/Amsterdam
HA_DOMAIN=xxx.xxx.com
LETSENCRYPT_EMAIL=admin@xxx.xxx.com
Small file, big energy.
Step 3 – Create the final docker-compose.yml
File path:
/docker/ha-proxy/docker-compose.yml
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:latest
container_name: nginx-proxy
restart: unless-stopped
network_mode: host
volumes:
- certs:/etc/nginx/certs:ro
- html:/usr/share/nginx/html
- ./nginx/vhost.d:/etc/nginx/vhost.d:rw
- /var/run/docker.sock:/tmp/docker.sock:ro
acme-companion:
image: nginxproxy/acme-companion:latest
container_name: nginx-proxy-acme
restart: unless-stopped
depends_on:
- nginx-proxy
environment:
DEFAULT_EMAIL: ${LETSENCRYPT_EMAIL}
NGINX_PROXY_CONTAINER: nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- html:/usr/share/nginx/html
- ./nginx/vhost.d:/etc/nginx/vhost.d:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
homeassistant:
image: ghcr.io/home-assistant/home-assistant:stable
container_name: homeassistant
restart: unless-stopped
privileged: true
network_mode: host
environment:
TZ: ${TZ}
VIRTUAL_HOST: ${HA_DOMAIN}
LETSENCRYPT_HOST: ${HA_DOMAIN}
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
VIRTUAL_PORT: 8123
volumes:
- ./homeassistant/config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro
volumes:
certs:
html:
acme:
A few important notes here:
nginx-proxyruns innetwork_mode: host- Home Assistant also runs in
network_mode: host acme-companionis explicitly told which Nginx container to use- there are no
ports:lines for host-mode services because they bind directly to the host stack
This arrangement turned out to be the key to a stable build.
Step 4 – Create the Nginx location override
File path:
/docker/ha-proxy/nginx/vhost.d/lin-ha.vanudengroup.com_location_override
Contents:
location / {
proxy_pass http://127.0.0.1:8123;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
}
And yes, the filename matters a lot.
It must end in:
_location_override
Not just the hostname.
Not _location.
Not “close enough.”
This is one of those tiny details that can eat an entire evening if you get it wrong.
Step 5 – Configure Home Assistant for reverse proxy support
File path:
/docker/ha-proxy/homeassistant/config/configuration.yaml
Minimal working version:
default_config:
http:
use_x_forwarded_for: true
trusted_proxies:
- 127.0.0.1
- ::1
ip_ban_enabled: true
login_attempts_threshold: 5
If you are using the normal include-based structure, this is also perfectly fine:
default_config:
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
http:
use_x_forwarded_for: true
trusted_proxies:
- 127.0.0.1
- ::1
ip_ban_enabled: true
login_attempts_threshold: 5
Important:
- only use one
http:block - do not duplicate sections
- because Nginx is now in host mode,
127.0.0.1and::1are the correct trusted proxy sources
That is what allows Home Assistant to accept forwarded headers correctly.
Step 6 – Start the stack
From the project directory:
cd /docker/ha-proxy
sudo docker compose up -d
If you want to rebuild from scratch:
cd /docker/ha-proxy
sudo docker compose down
sudo docker compose up -d
Nice, predictable, and satisfying.
Step 7 – Check that everything is alive
See the running containers:
sudo docker ps
Follow the Home Assistant logs:
sudo docker logs -f homeassistant
Follow the Nginx logs:
sudo docker logs -f nginx-proxy
Follow the Let’s Encrypt logs:
sudo docker logs -f nginx-proxy-acme
At this point, the stack should come up cleanly and the certificate should be issued automatically.
Step 8 – DNS and router setup
This part still matters, because no amount of Docker enthusiasm can fix bad DNS.
You need:
- an
Arecord forlin-ha.vanudengroup.compointing to your public IP - port forwarding on the router:
- TCP 80 →
192.168.1.166:80 - TCP 443 →
192.168.1.166:443
- TCP 80 →
Do not expose port 8123 directly to the internet.
That is the reverse proxy’s job.
Step 9 – Allow the traffic through the firewall
If ufw is active on the Ubuntu host:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Otherwise you will have a beautifully configured service that nobody can actually reach, which is a very traditional IT move.
Step 10 – Useful validation tests
Test Home Assistant locally
curl -I http://127.0.0.1:8123
Expected result:
HTTP/1.1 405 Method Not Allowed
Allow: GET
That is fine. curl -I sends a HEAD request, and Home Assistant is basically replying with: “I’m here, but use GET like a civilized person.”
Test HTTP via the public hostname
curl -I --resolve xxx.xxx.com:80:127.0.0.1 http://xxx.xxx.com
Expected result:
HTTP/1.1 301 Moved Permanently
Location: https://xxx.xxx.com/
Exactly what we want.
Test HTTPS via the public hostname
curl -kI --resolve xxx.xxx.com:443:127.0.0.1 https://xxx.xxx.com
Expected result:
HTTP/2 405
Also fine. That proves:
- TLS is working
- Nginx is working
- proxying is working
- Home Assistant is answering behind HTTPS
In other words: the whole chain is alive.
Problems we ran into so you do not have to
1. acme-companion could not find nginx-proxy
The fix was this:
environment:
DEFAULT_EMAIL: ${LETSENCRYPT_EMAIL}
NGINX_PROXY_CONTAINER: nginx-proxy
Without that, acme-companion complained loudly and usefully, which is nice once and less nice the seventh time.
2. Duplicate mount points on vhost.d
Do not mount the same path twice using both:
- a named Docker volume
- and a bind mount
Docker is absolutely correct to reject that, and it will do so with all the warmth of a failed config commit.
3. Wrong override filename
This one was sneaky.
The correct file is:
xxx.xxx.com_location_override
Not the hostname alone.
Not _location.
The exact suffix matters.
4. Home Assistant recovery mode
If you reference:
automations.yamlscripts.yamlscenes.yaml
and those files do not exist, Home Assistant will refuse to load the config normally.
Easy fix: create them.
5. Reverse proxy trust
If trusted_proxies is wrong, Home Assistant will not correctly accept the forwarded request path.
In this final design, the correct trusted proxies are:
trusted_proxies:
- 127.0.0.1
- ::1
That part matters more than it looks.
Why this final solution works so well
The earlier attempts failed because bridge-mode Nginx had trouble cleanly reaching a host-mode Home Assistant service in this specific setup.
Once both services were placed in host networking:
- the path became direct
- the proxy target became simple
- the config became easier to understand
- troubleshooting became dramatically less annoying
Nginx now just proxies to:
127.0.0.1:8123
That is the kind of solution that makes a network engineer smile softly and mutter, “yes, that’s better.”
Final test
Open this in your browser: xxx.xxx.com
Then test it from outside your LAN as well, for example from your phone on 4G or 5G.
If that works, congratulations: you now have Home Assistant running in Docker host mode, neatly published behind Nginx, with valid Let’s Encrypt certificates and no ugly exposed port 8123.
That is a good day in the server room.
Handy admin commands
Restart only Home Assistant
cd /docker/ha-proxy
sudo docker compose restart homeassistant
Restart the full stack
cd /docker/ha-proxy
sudo docker compose restart
Rebuild everything
cd /docker/ha-proxy
sudo docker compose down
sudo docker compose up -d
Clear Home Assistant IP bans
sudo rm -f /docker/ha-proxy/homeassistant/config/ip_bans.yaml
cd /docker/ha-proxy
sudo docker compose restart homeassistant
Final thoughts
So yes, Home Assistant in Docker host mode behind nginx-proxy with Let’s Encrypt works perfectly well.
The trick is not to overcomplicate the network path.
Once we stopped trying to make a bridge-mode proxy talk politely to a host-mode service through side doors and half-open windows, the whole thing settled down nicely.
Which, frankly, is the kind of result every cheerful network engineer wants:
less swearing, more uptime, and one more service humming along happily behind a green padlock.
