Home Assistant in Docker with Nginx and Let's Encrypt on Raspberry Pi

This guide will help to you to setup home assistant as a docker container and make it publicly available behind a ngix proxy, all running in containers. Additionally, we'll use certbot to keep your ssl certificates in sync and Duck DNS so that you access it even with a dynamic ip address.

There we go... Dockerized Home Assistant behind Nginx with Let's Encrypt and DuckDNS :)

This guide is partially based on the generic guide on how to install Ngix and Let's Encrypt by Philipp (Nginx and Let’s Encrypt with Docker in Less Than 5 Minutes | by Philipp | Medium)

In order to start, please make sure you have the following setups done already

  • Home Assistant installed as docker container and available under port 8123
  • Create a DuckDNS account and register your subdomain
  • You know how to redirect a port from your router to the computer running docker
We'll be using docker-compose, which is much simpler to use than the plain docker cli. If you are not familiar with docker-compose, please read Overview of Docker Compose | Docker Documentation

Let's get SSL started!

Nginx and the other docker services will be part of the same docker-compose.yaml file. As a minimal setup, we'll create the following. I suggest to put that file somewhere in a subfolder called "proxy". at the end of this tutorial, we will have quite a bit of configuration files there.

version: '3' services: nginx: image: arm64v8/nginx ports: - "80:80" volumes: - ./data/nginx:/etc/nginx/conf.d:ro - ./data/wwwroot:/var/www/root:ro

This is a very basic setup, which we will use to test the connectivity to your machine. This will be required to make sure the certificate request will succeed.

The most interesting bits are the volume mappings. We will use a directory structure like the one below
  • docker-compose.yaml: The file above
  • ./data/nginx: The configuration for nginx, basically the file app.conf
  • ./data/wwwroot: content of the default bindings (port 80 and 443), like an index.html

Make Nginx available from internet

Let's create the simplest possible configuration and put that under ./data/nginx/, so that we can test the connectivity to our machine.

server { listen 80; server_name yourdomain.duckdns.org;         #replace this location / { root /var/www/root; } }

Note the line 3, and replace the domain with your own name!

Let's also put a welcome index.html under the folder ./data/wwwroot to that we can test our network configuration.

<html> <body> <h1>Welcome</h1> It works! </html>T

Yes, minimalistic, but will serve its purpose. Right now your folder should look like this:

ubuntu@rpiuntu:/home/docker/proxy$ tree . ├── data │   ├── nginx │   │   └── app.conf │   └── wwwroot │   └── index.html └── docker-compose.ymlT

Let's bring it up and test if the server is available from local and remote.
Use docker-compose up to start and see the log in the command line. Exit with Ctrl-C.

Your index page should now be available under port 80 of your raspberry and from the internet using http://yourdomain.duckdns.org. You can use a tool like Visualping: #1 Website change detection, monitoring and alerts to check your configuration. 

Using visualping.io to check accessibility from remote

Of course you can use your mobile to test it as well. I'm not using my mobile for such tests, to avoid any caching or related issues. 

If there are any troubles accessing the domain, please check again your firewall and NAT/Port Forwarding configuration in your router.

Switch to SSL

We are not using SSL for now. Let's change that. First, let's extend the docker-compose.yaml with additional configurations for certbot add sharing of results from certbot to ngix. Below the updated file, changes in bold

version: '3' services: nginx: image: arm64v8/nginx ports: - "80:80" - "443:443"                                     # added volumes: - ./data/nginx:/etc/nginx/conf.d:ro - ./data/wwwroot:/var/www/root:ro - ./data/certbot/conf:/etc/letsencrypt:ro       # added - ./data/certbot/www:/var/www/certbot:ro      # added certbot:                                            # added image: certbot/certbot:arm64v8-latest             # added volumes:                                         # added - ./data/certbot/conf:/etc/letsencrypt         # added - ./data/certbot/www:/var/www/certbot           # added

As you can see, we share a couple of volumes to both ngix and certbot
  • ./data/certbot/conf: All the configuration for the certbot
  • ./data/certbot/www: Public accessible challenges folder
The first folder is the store for the actual certificates which the certbot receives from Let's Encrypt. Hence it makes sense that the certbot writes into that folder, and the ngix webserver reads from it.

The other folder is related to how Let's Encrypt validates the ownership of the domain. It's used to store challenges and must be available from the public. It serves as validation that the domain is actually under he control of computer that is requesting for that certificate. 

Let's extend the configuration for ngnix, so that
  1. Serves the challenges under the challenges folder when asked
  2. Serves the index.html under port 443 with ssl and the given certificates
See the related changes below, highlighted in bold

server { listen 80; server_name yourdomain.duckdns.org;         # replace this location /.well-known/acme-challenge/ { # added root /var/www/certbot;                 # added }                                           # added location / { root /var/www/root; } } server { listen 443 ssl; server_name yourdomain.duckdns.org; location / { root /var/www/root; } ssl_certificate /etc/letsencrypt/live/##REPLACE##/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/##REPLACE##/privkey.pem; #Optional: Only works with Philipp's script (see below) include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; }

Please note the additional entry, which specifies another endpoint. It will use the certificates from certbot. Please replace the variable ##REPLACE## with the name of your full domain, for example yourname.duckdns.org.

Starting the server would now cause errors, since the certificates are not yet installed. Hence ngnix would not start. To solve this, there are two options
  1. Comment out the second server and wait until certificates are downloaded via certibot
  2. Use the script from Philipp's Article which generates a temporary certificate for the first start
Using Philipp's script also has the advantage that it sets up the certbot automatically after you modify the necessary variables in the script. Please follow his instructions to
  1. Download the script
  2. Edit the script to include your domain and e-mail address
  3. Run the script
Horray! After a docker-compose up you should be serving your website under http and https!

Setup Certificate Renewal

We can use the excellent work from Philipp one more time and adjust our docker-compose.yaml for one last time.

To periodically check for certificate renewal, the following line is inserted to the service configuration of certbot

entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

To reload the certificates if necessary, we will extend the nginx configuration with the following addon

command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\

This gives us the final configuration for the docker-compose.yaml file for these two services and brings us to the full configuration below

version: '3' services: nginx: image: arm64v8/nginx ports: - "80:80" - "443:443"                                      volumes: - ./data/nginx:/etc/nginx/conf.d:ro - ./data/wwwroot:/var/www/root:ro - ./data/certbot/conf:/etc/letsencrypt:ro        - ./data/certbot/www:/var/www/certbot:ro      command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" certbot:                                             image: certbot/certbot:arm64v8-latest             volumes:                                         - ./data/certbot/conf:/etc/letsencrypt         - ./data/certbot/www:/var/www/certbot           entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

Update DuckDNS Entries automatically

In order to update the DuckDNS entries automatically, we will add yet another very small docker container, which will simply check the external IP address and update the domain if necessary.

For that, we would need the token related to your domain, which you can get from the duckdns.org website under: Duck DNS (www.duckdns.org).

We'll add the following configuration to the docker-compose file, according to LinuxServer.io / docker-duckdns · GitLab

version: '3' services: duckdns:                                             image: lscr.io/linuxserver/duckdns:latest             environment: - TZ=Asia/Singapore - SUBDOMAINS=##REPLACE## - TOKEN=##REPLACE## - LOG_FILE=true volumes: - ./duckdns:/config restart: unless-stopped

Make Home Assistant Available via Reverse Proxy

There are two things we need to adjust in our configuration to make HA work behind a proxy. First of all, we would have to setup a new server in ngix, which forwards all traffic to the internal IP where Home Assistant is hosted. In my case, I host the proxy on the port 8124. If you wanna make your HA externally accessible under the same port, you could adjust your NAT configuration accordingly.

nginx and docker

This is the configuration in app.conf for nginx, which can live side by side with all the previous configurations.

server { listen 443 ssl; server_name yourdomain.duckdns.org; location / { proxy_pass; #YOUR Address and Port of HA proxy_set_header Host $host; proxy_redirect http:// https://; proxy_http_version 1.1; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } ssl_certificate /etc/letsencrypt/live/##REPLACE##/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/##REPLACE##/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; }

As you can see, the configuration is almost the same as for the test https site we created before, except that instead serving files from the local file system, we load data from an internal host, and of course the port configuration.

Don't forget to make the new port accessible for the docker container, see below

version: '3' services: nginx: image: arm64v8/nginx ports: - "80:80" - "443:443" - "8124:8124"

For the app.config change, a docker restart would be enough, but to apply also the docker changes, we will use docker-compose up -d. It will regenerate the ngnix container and restart with the new configuration.

Home Assistant

As a last step, we have to help Home Assistant to work property behind a proxy server. Add the following configuration to the configuration.yaml of Home Assistant to
  • Make sure Home Assistant can identify the IP of the client
  • Allows the revers proxy to connect to the endpoint

 # Loads default set of integrations. Do not remove. default_config: # Text to speech tts: - platform: google_translate automation: !include automations.yaml script: !include scripts.yaml scene: !include scenes.yaml http: use_x_forwarded_for: true        # How to extract client IP address trusted_proxies:                 # Permit connections from reverse proxy -

That's it, le me know in the comments if that was helpful.


Popular posts from this blog

Use Bodmer TFT_eSPI Library with PlatformIO

Migrating from Arduino IDE to Visual Studio Code to PlatformIO

Powerful Image to PDF Conversion with OCR for free with WSL