Setup SSL with Docker, NGINX and Lets Encrypt
Did you ever want to secure your application with the HTTPS protocol? This guide will show you how to run your applications behind a reverse proxy and secure the communication with HTTPS by using Docker, NGINX, and Lets Encrypt.
Used Technologies
- Lets Encrypt: Get free and automated SSL certificates for your applications
- NGINX: Reverse proxy to secure your web applications
- Docker: Host your applications and make them public to the web behind NGINX
With these three technologies, you can create a secure environment to publish your applications to the web. NGINX will be the entry point for users from the web to access the different applications. The SSL certificates are needed to use HTTPS as a communication protocol between your server and the clients.
Docker itself will host NGINX, your applications, and a service to generate new Lets Encrypt certificates automatically.
VPS Hosting Course
Learn everything you need to know about servers and hosting your own applications!To follow this guide, you need a domain, and you need to install docker and docker-compose for your system!
Steps
You can receive SSL certificates for any application you want with the following steps.
- Create your application with Docker
- Create a reverse proxy with NGINX
- Automate SSL certificates with Certbot
Create your application with Docker
The first step is to use docker compose to create a container for your application. I will use the simple helloworld image found here.
Need help or want to share feedback? Join my discord community!
services:
helloworld:
container_name: helloworld
image: crccheck/hello-world
ports:
- 80:8000
The tasks of the different attributes are:
- container_name: name the service to better handle it
- image: node image to start the node application
- ports: map port 8000 to 80, to access it in the browser under the servers ip(! this will be removed later)
Now you can create the container by running:
If this guide is helpful to you and you like what I do, please support me with a coffee!
docker compose up -d helloworld
After this, you can reach your application on the host machine by entering the IP of your server. Don’t make your port available if you run this on a machine on the Internet because it is unsecured. To make this secure will create a reverse proxy with NGINX and put it in front of our applications.
Create a reverse proxy with NGINX
First, we will create a configuration to pass the requests to our node application via NGINX. Therefore we will make the directory nginx containing the file nginx.conf. With this file, we configure the NGINX instance.
events {
worker_connections 1024;
}
http {
server_tokens off;
charset utf-8;
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://helloworld:8000/;
}
}
}
The event section is needed to run NGINX. The http section is the interesting part for us. First, the server is defined to listen to all requests on port 80 and is set as a default_server for all requests to this host. After that, we say each server name is valid for requests. In a later part, you can define different domains that are used for a server.
In the location / we define that every request should be passed to our helloworld container with port 8000. It is important to say that the specified port is not the public but the private port. Therefore we will remove the port mapping in the docker-compose.yml file.
Additionally, we will create an NGINX container in the same file.
services:
helloworld:
container_name: helloworld
image: crccheck/hello-world
expose:
- 8000
nginx:
container_name: nginx
restart: unless-stopped
image: nginx
ports:
- 80:80
- 443:443
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
We will make ports 80 and 443 public because these are used for HTTP and HTTPS.
The volume is used to make the configuration available inside the container and to persist any changes.
You can create and update the container by running:
docker compose up -d
After this, you should be able to open the application by entering the URL to the host. Now we have setup a reverse proxy for our application. The next step will be to get SSL certificates and redirect all HTTP requests to HTTPS.
Automate SSL certificates with Certbot
Let’s Encrypt works with challenges to check if the domain and the host are eligible. For the challenges, we have to create a route called /.well-known/acme-challenge/ in the NGINX configuration. This will be part of the HTTP section, but we will move it there after switching to HTTPS, so the automated renewal works as expected.
server {
listen 80 default_server;
.
.
.
location ~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
As you can see, it points to the directory /var/www/certbot. Therefore, we need to add two volumes to make the challenges and the resulting certificates available in the NGINX container.
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
With this done, we have to recreate the container:
docker compose up -d nginx
After configuring NGINX, we can retrieve the certificates. To do this, we will use the certbot container and run it with a set of parameters.
certbot:
image: certbot/certbot
container_name: certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
command: certonly --webroot -w /var/www/certbot --force-renewal --email {email} -d {domain} --agree-tos
The parameters are used to automate the process without you needing to manually enter the data on every container run.
- certonly: only generate certificate without installing
- webroot: use our own webserver in this case NGINX
- w: root directory of webserver for the challenges
- force-renewal: on repeated run renew certificates
- email: your email for notifcations
- d: domain for the certificate (you can enter -d {domain} multiple times for different domains)
- agree-tos: agree the terms of service automatically (dont set it, if you want to read and understand them first)
Before requesting the certificate, we need to create an A record at our hosting provider pointing to our server. I am using Hostinger as my domain provider (find a domain here – affiliate link):
- Manage Domain
- DNS / Nameserver
- Manage DNS Records > Create A record:
- Type = A
- Name = @ – (This means it is for the main domain)
- Points to = IP of your server
- TTL = 14400
Afterward, we can request the certificates by creating the container with:
docker compose up -d certbot
By running the command docker logs certbot
you can see if everything worked out and if you received your certificate. After you receive it, you have to include the certificate in nginx.conf
.
Configure HTTPS in NGINX
In the first step, we redirect all HTTP requests to HTTPS, and in the second step, we create the HTTPS section for our application:
http {
server_tokens off;
charset utf-8;
# always redirect to https
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
# use the certificates
ssl_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{domain}/privkey.pem;
server_name {domain};
root /var/www/html;
index index.php index.html index.htm;
location / {
proxy_pass http://helloworld:8000/;
}
location ~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
}
Another thing you can see is that we moved the challenge route to the HTTPS section. We need to do this because all new requests of Lets Encrypt will also be redirected to HTTPS.
Automated renewal with crontab
The last step is to automatically renew the certificates before they run out. A certificate has a lifetime of 90 days, and it is recommended to update them after a timespan of 60 days. Therefore, we need to rerun our certbot container every 60 days to renew the certificates. I will accomplish this by using crontab.
A crontab can be created on linux systems by running:
crontab -e
And adding a line with the following structure:
0 5 1 */2 * /usr/bin/docker compose -f /var/docker/docker-compose.yml up certbot
The command means: Run docker-compose up -d at 5 am on the first day every 2nd month.
After implementing this guide, you should have an application behind a reverse proxy accessible via the HTTPS protocol!
Thank you for reading my guide. If you have any questions feel free to shoot me an email at mail@programonaut.com, and I will try to help you!
In case you liked this post consider subscribing to my newsletter. You will also receive a free docker-compose cheat sheet!
Discussion (20)
-
-
pl4typus2nd
with certbot, to get rid of interactive prompt for TOS append --agree-tos flag: command: certonly -v -n --agree-tos --webroot -w /var/www/certbot --force-renewal
-
Programonaut
Thank you for the hint, I will update the guide accordingly!
-
Mike
FYI If you are running this on an ARM based server the default certbot/certbot image will NOT work and will provide an "exec format error". INSTEAD use the image: certbot/certbot:arm32v6-latest
-
Eric
Do NOT run this long term with the "--force-renewal" parameter. As outlined on Certbot's documentation (https://eff-certbot.readthedocs.io/en/stable/using.html), if you run with --force-renewal, it will renew the cert every single time it runs and you will very quickly run into the Let's Encrypt rate limit. Once you have the certificate, you should switch to the renew command.
-
SapuSeven
This is the crontab entry I use: `0 3 1 * * docker start $(docker ps -q -f status=exited -f ancestor=certbot/certbot)` It looks for all stopped certbot containers and starts them once per month.
-
Phil Jones
Excellent guide, thank you. I don't think the cron command is quite correct. My version had a different docker-compose binary location (easily found with which docker-compose), and I also needed to add -d to the command for it to work. Thanks and best regards Phil J e.g. 0 5 1 */2 * /usr/bin/docker-compose -f {path_to_compose_yml}/docker-compose.yml up -d certbot
-
Bernhard Kreuz
Thanks a lot for the tutorial! it could need a small update like what SapuSeven and Eric suggested, along with removing http2 as its not supported anymore. all in all, great! thanks a lot. The first tutorial that really made my setup work :)
-
Rodislav
Little correction: the "up" is missplaced Instead of: /usr/bin/docker compose -f up /var/docker/docker-compose.yml certbot Use: /usr/bin/docker compose -f /var/docker/docker-compose.yml up certbot
-
Programonaut
Thank you, I corrected it!
-
-
Susan T
great post, it was super helpful!
-
Programonaut
Glad I could help!
-
-
Sam
Hello, first of all thank you for the tutorial, simple and efficient. Now it's the first time my cert was renewed with the cron, but I noticed that I had to manually restart nginx container to pickup new certs. Did I miss something?
-
Programonaut
Hi, glad it helped till now! When I had the setup myself it worked automatically as the volumes are shared and contain the files. If it only works with a reboot of nginx you could adapt your Cron command to restart it. Let me know if it helped!
-
-
Carlos M
I was a bit confused on the volumes nginx needs mounted. You show one at the begining, later you mention 2 more. So is nginx suppose to have a total of 3 mounts: volumes: - ./nginx/conf/:/etc/nginx/conf.d/ - ./certbot/www/:/var/www/certbot/ - ./certbot/conf:/etc/letsencrypt/ If so, under letsencrypt, which is certbot/conf... potentially nginx will see all the data including meta data, account data and renewal folders.
-
Tanmay Rane
Amazing tutorial, Thanks Maximilian !! could you please provide updated command without "--force-renewal" and with "renew"
-
Programonaut
Hello, I will try to do so in the near future.
-
-
jesse
I have no idea what I'm doing wrong. I'm running a server with my docker containers on it locally on my network which I accessed via duck.dns and everything was working fine before running the certbot but after I ran the certbot I have an SSL certificate on the my site when I access it from my local network but it says the certificate is not valid. and no matter what I try I cannot access the nginx from my duck.dns even though I opened port 80 and 443 on my router.
-
Programonaut
Hello, sadly I do not know what the problem might be.
-
-
Swedge
Good tutorial. Helpful!
this was very helpful tutorial. thank you!