Docker with Nginx and Let's Encrypt
Posted Wednesday, February 10, 2021.Serving web content over https is a must these days. If your webserver is a Docker container, it might be tempting to install nginx and Let's Encrypt in that image. However your image would no longer be encapsulating a single function. Instead, we can use multiple containers and Docker Compose to achieve our goal.
Docker Compose
First, let's take a look at the docker-compose.yml setup and then discuss each part. In this example we use 4 containers:
- certbot/certbot:latest- Let's Encrypt container for certificates
- nginx:alpine- nginx webserver using the slim alpine variant
- hiimtmac/api:latest(fake api container)
- hiimtmac/web:latest(fake web container)
Note:
- Both apiandwebcontainers are not necessary, you could have one or the other.
- In this example, the webcontainer talks to theapicontainer, but theapicontainer is also available for external services.
- I chose to have both webandapiuse the same domain/subdomain. This is also not required, but you would need separate server blocks if the domains/subdomains were to be handled differently.
- Volumes are mounted in ro(read only) mode wherever possible
File Setup:
Normally, you would not have runtime configuration for a React application as it builds in all environment variables when compiling. However, in a previous post, we discussed how this could be achieved, hence the .env.web environment file.
Nginx Setup
We will be using the template feature of the nginx docker image, which will allow us to have some runtime conditions and variables (this is not necessary, if you do not want to use templates you can map your nginx configs right to /etc/nginx/conf.d/).
nginx.conf.template
Let's break it down into smaller pieces.
The default server is setup to return 404 on any server name by default. We setup listeners on IPV4 and IPV6 on both port 80 and port 443 with ssl and http2. We include the ssl.conf file as it is needed for https traffic.
The 80 server block accepts IPV4 and IPV6 traffic on port 80 (regular http) for server name we specified in the docker-compose.yml file. In this example it would resolve to server_name hiimtmac.com;. The first location block is used to handle certificates from Let's Encrypt. The second location block is used to redirect and upgrade any http traffic to https - ie sending it to the 443 server block.
Similar to the 80 server block, the 443 server block block accepts IPV4 and IPV6 traffic on the server name we specified in the docker-compose.yml file (hiimtmac.com;). Instead of port 80, it uses port 443 and specifies ssl and http2. We include the ssl.conf file which is needed for https traffic. We also include the proxy.conf file. The contents of each of these files could be inlined, but it groups functionality together and makes the main conf file slightly shorter. The resolver line is necessary as we need Docker's internal DNS for routing to our containers.
The location /api/ block will send any matching url to our backend api container. For example hiimtmac.com/api/test and hiimtmac.com/api/cool would match, however hiimtmac.com/test/api would not. This way, the web container could make a call to hiimtmac.com/api/users and it would be routed to the api container, and not call itself. When we are bringing up our containers to get certificates (in the next section), we want the nginx configuration to be valid even when the web and api containers are not live. In order to satisfy this need, we use set $upstream_api api; and then proxy_pass http://$upstream_api:8080 as opposed to just proxy_pass http://api:8080. This way we don't have to start all the containers in order for nginx to be happy.
The location / block will catch anything that our location /api/ block did not. We have set the root to what we mapped in from the web container, which we can then use for try_files. This will attempt to serve static files from the specified directory before falling back to the web container. We use the @proxy block to achieve the same goal as the api block for the proxy_pass.
includes/ssl.conf.template
This configuration file will substitute the server name from your compose file (in this case hiitmac.com), as well as provide the configurations that nginx needs (we will download in next section) to work with Let's Encrypt to serve https traffic.
includes/proxy.conf.template
This is more basic information. You can add advanced settings as well.
General
Additionally, you will want to include other settings, for example the options below:
You can either inline them into your nginx.conf.template or you could make them as a separate file in the includes directory, and then include them in the 443 block along where you link the ssl.conf and proxy.conf. I am by no means an expert at nginx configuration, but this has worked for me so far.
Certbot Setup
I got a great deal of information from GitHub user wmnnd's repo on setting this up, and have since made some tweaks myself. I've separated it into 3 steps:
- Setup - download configurations, setup folders, create keys
- Staging - try to get a ssl certificate from Let's Encrypt, but in staging mode (production only allows a certain amount of attempts per day so tinkering with settings will block you after a while)
- Production - do it for real and get the production certificate
I would recommend creating a scripts directory alongside your data directory with the following 3 files (if you don't house them there, be sure to adjust the cd ../ command in the following scripts).
Setup
The setup.sh script looks like this:
When you run it, you will include the root url for the certificate name. In our example, I would run this script with ./setup.sh hiimtmac.com.
Staging
The staging.sh script looks like this:
When you run it, you will include the root url for the certificate name, but also you have the option to include other domains in the certificate. In our example, I would run this script with ./staging.sh hiimtmac.com, but I could also do ./staging.sh hiimtmac.com blog.hiimtmac.com (and then I would need to include the blog subdomain somewhere in the nginx config).
Production
The production.sh script looks like this:
The production script is almost the same as the staging one, except there are a couple parameters changed when grabbing the certificate, such as providing an email. This script would be used as such: ./production.sh taylor@hiimtmac.com hiimtmac.com.
When you run this, it will generate a production SSL certificate for you. Now you can run docker-compose up -d and serve your content with https! The custom entrypoint for the certbot image will handle renewals for you so you do not have to worry about that.
Volumes
In order to serve static content from the web container through nginx, we created a volume and then mounted it to the web and nginx containers. This creates a volume in which the content from the web's public folder can then be accessed by the nginx container. I'm not sure this is the best way to achieve this goal as it comes with a few caviats:
- You can't mount this directory to the host. If you do, the empty files of the host will overwrite the contents of the webcontainer, and then thenginxcontainer will just see an empty directory.
- When re-deploying changes, you need to docker-compose down -v(remove volumes) beforedocker-compose up -d. If you don't, then thenginxcontainer still has a copy of those files in it's container, and overwrites the newer files of thewebcontainer when you bring it up.
- If your api container included a public directory and you were not using a separate front-end container, you would map its public folder instead of how we did with the webcontainer.
If you don't need this functionality, you can delete the shared-data volume from the compose file, the volume section from both web and nginx containers, and the root/try_files parts of the nginx.conf.template file.
In Closing
This deployment has been resiliant and easily replicatable for me in multiple production servers. I've configured it with the following setups without much change between configs:
- Nginx, Certbot, Laravel*
- Nginx, Certbot, Rails (puma)
- Nginx, Certbot, Vapor
- Nginx, Certbot, Vapor, React
*The Laravel config for nginx was different because of php-fpm.
I had to figure this stuff out from a lot of trial and error, and research. If there is a better way to go about this, please let me know!
Tagged With: