Tutorial

How To Secure a Containerized Node.js Application with Nginx, Let's Encrypt, and Docker Compose

How To Secure a Containerized Node.js Application with Nginx, Let's Encrypt, and Docker Compose

Introduction

There are multiple ways to enhance the flexibility and security of your Node.js application. Using a reverse proxy like Nginx offers you the ability to load balance requests, cache static content, and implement Transport Layer Security (TLS). Enabling encrypted HTTPS on your server ensures that communication to and from your application remains secure.

Implementing a reverse proxy with TLS/SSL on containers involves a different set of procedures from working directly on a host operating system. For example, if you were obtaining certificates from Let’s Encrypt for an application running on a server, you would install the required software directly on your host. Containers allow you to take a different approach. Using Docker Compose, you can create containers for your application, your web server, and the Certbot client that will enable you to obtain your certificates. By following these steps, you can take advantage of the modularity and portability of a containerized workflow.

In this tutorial, you will deploy a Node.js application with an Nginx reverse proxy using Docker Compose. You will obtain TLS/SSL certificates for the domain associated with your application and ensure that it receives a high security rating from SSL Labs. Finally, you will set up a cron job to renew your certificates so that your domain remains secure.

Prerequisites

To follow this tutorial, you will need:

  • An Ubuntu 18.04 server, a non-root user with sudo privileges, and an active firewall. For guidance on how to set these up, please read this Initial Server Setup guide.

  • Docker and Docker Compose installed on your server. For guidance on installing Docker, follow Steps 1 and 2 of How To Install and Use Docker on Ubuntu 18.04. For guidance on installing Compose, follow Step 1 of How To Install Docker Compose on Ubuntu 18.04.

  • A registered domain name. This tutorial will use your_domain throughout. You can get one for free at Freenom, or use the domain registrar of your choice.

  • Both of the following DNS records set up for your server. You can follow this introduction to DigitalOcean DNS for details on how to add them to a DigitalOcean account, if that’s what you’re using:

    • An A record with your_domain pointing to your server’s public IP address.
    • An A record with www.your_domain pointing to your server’s public IP address.

Once you have everything set up, you’re ready to begin the first step.

Step 1 — Cloning and Testing the Node Application

As a first step, you’ll clone the repository with the Node application code, which includes the Dockerfile to build your application image with Compose. Then you’ll test the application by building and running it with the docker run command, without a reverse proxy or SSL.

In your non-root user’s home directory, clone the nodejs-image-demo repository from the DigitalOcean Community GitHub account. This repository includes the code from the setup described in How To Build a Node.js Application with Docker.

Clone the repository into a directory. This example uses node_project as the directory name. Feel free to name this directory to your liking:

  1. git clone https://github.com/do-community/nodejs-image-demo.git node_project

Change into the node_project directory:

  1. cd node_project

In this directory, there is a Dockerfile that contains instructions for building a Node application using the Docker node:10 image and the contents of your current project directory. You can preview the contents of the Dockerfile with the following:

  1. cat Dockerfile
Output
FROM node:10-alpine RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app COPY package*.json ./ USER node RUN npm install COPY --chown=node:node . . EXPOSE 8080 CMD [ "node", "app.js" ]

These instructions build a Node image by copying the project code from the current directory to the container and installing dependencies with npm install. They also take advantage of Docker’s caching and image layering by separating the copy of package.json and package-lock.json, containing the project’s listed dependencies, from the copy of the rest of the application code. Finally, the instructions specify that the container will be run as the non-root node user with the appropriate permissions set on the application code and node_modules directories.

For more information about this Dockerfile and Node image best practices, please explore the complete discussion in Step 3 of How To Build a Node.js Application with Docker.

To test the application without SSL, you can build and tag the image using docker build and the -t flag. This example names the image node-demo, but you are free to name it something else:

  1. docker build -t node-demo .

Once the build process is complete, you can list your images with docker images:

  1. docker images

The following output confirms the application image build:

Output
REPOSITORY TAG IMAGE ID CREATED SIZE node-demo latest 23961524051d 7 seconds ago 73MB node 10-alpine 8a752d5af4ce 3 weeks ago 70.7MB

Next, create the container with docker run. Three flags are included with this command:

  • -p: This publishes the port on the container and maps it to a port on your host. You will use port 80 on the host in this example, but feel free to modify this as necessary if you have another process running on that port. For more information about how this works, review this discussion in the Docker documentation on port binding.
  • -d: This runs the container in the background.
  • --name: This allows you to give the container a memorable name.

Run the following command to build the container:

  1. docker run --name node-demo -p 80:8080 -d node-demo

Inspect your running containers with docker ps:

  1. docker ps

The following output confirms that your application container is running:

Output
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4133b72391da node-demo "node app.js" 17 seconds ago Up 16 seconds 0.0.0.0:80->8080/tcp node-demo

You can now visit your domain to test your setup: http://your_domain. Remember to replace your_domain with your own domain name. Your application will display the following landing page:

Application Landing Page

Now that you have tested the application, you can stop the container and remove the images. Use docker ps to get your CONTAINER ID:

  1. docker ps
Output
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4133b72391da node-demo "node app.js" 17 seconds ago Up 16 seconds 0.0.0.0:80->8080/tcp node-demo

Stop the container with docker stop. Be sure to replace the CONTAINER ID listed here with your own application CONTAINER ID:

  1. docker stop 4133b72391da

You can now remove the stopped container and all of the images, including unused and dangling images, with docker system prune and the -a flag:

  1. docker system prune -a

Press y when prompted in the output to confirm that you would like to remove the stopped container and images. Be advised that this will also remove your build cache.

With your application image tested, you can move on to building the rest of your setup with Docker Compose.

Step 2 — Defining the Web Server Configuration

With our application Dockerfile in place, you’ll create a configuration file to run your Nginx container. You can start with a minimal configuration that will include your domain name, document root, proxy information, and a location block to direct Certbot’s requests to the .well-known directory, where it will place a temporary file to validate that the DNS for your domain resolves to your server.

First, create a directory in the current project directory, node_project, for the configuration file:

  1. mkdir nginx-conf

Create and open the file with nano or your favorite editor:

  1. nano nginx-conf/nginx.conf

Add the following server block to proxy user requests to your Node application container and to direct Certbot’s requests to the .well-known directory. Be sure to replace your_domain with your own domain name:

~/node_project/nginx-conf/nginx.conf
server {
        listen 80;
        listen [::]:80;

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;

        server_name your_domain www.your_domain;

        location / {
                proxy_pass http://nodejs:8080;
        }

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
}

This server block will allow you to start the Nginx container as a reverse proxy, which will pass requests to your Node application container. It will also allow you to use Certbot’s webroot plugin to obtain certificates for your domain. This plugin depends on the HTTP-01 validation method, which uses an HTTP request to prove that Certbot can access resources from a server that responds to a given domain name.

Once you have finished editing, save and close the file. If you used nano, you can do this by pressing CTRL + X, then Y, and ENTER. To learn more about Nginx server and location block algorithms, please refer to this article on Understanding Nginx Server and Location Block Selection Algorithms.

With the web server configuration details in place, you can move on to creating your docker-compose.yml file, which will allow you to create your application services and the Certbot container you will use to obtain your certificates.

Step 3 — Creating the Docker Compose File

The docker-compose.yml file will define your services, including the Node application and web server. It will specify details like named volumes, which will be critical to sharing SSL credentials between containers, as well as network and port information. It will also allow you to specify commands to run when your containers are created. This file is the central resource that will define how your services will work together.

Create and open the file in your current directory:

  1. nano docker-compose.yml

First, define the application service:

~/node_project/docker-compose.yml
version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped

The nodejs service definition includes the following:

  • build: This defines the configuration options, including the context and dockerfile, that will be applied when Compose builds the application image. If you wanted to use an existing image from a registry like Docker Hub, you could use the image instruction instead, with information about your username, repository, and image tag.
  • context: This defines the build context for the application image build. In this case, it’s the current project directory which is represented with the ..
  • dockerfile: This specifies the Dockerfile that Compose will use for the build — the Dockerfile reviewed at in Step 1.
  • image, container_name: These apply names to the image and container.
  • restart: This defines the restart policy. The default is no, but in this example, the container is set to restart unless it is stopped.

Note that you are not including bind mounts with this service, since your setup is focused on deployment rather than development. For more information, please read the Docker documentation on bind mounts and volumes.

To enable communication between the application and web server containers, add a bridge network called app-network after the restart definition:

~/node_project/docker-compose.yml
services:
  nodejs:
...
    networks:
      - app-network

A user-defined bridge network like this enables communication between containers on the same Docker daemon host. This streamlines traffic and communication within your application, since it opens all ports between containers on the same bridge network, while exposing no ports to the outside world. Thus, you can be selective about opening only the ports you need to expose your frontend services.

Next, define the webserver service:

~/node_project/docker-compose.yml
...
 webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

Some of the settings defined here for the nodejs service remain the same, but some of the following changes were made:

The following named volumes and bind mounts are also specified:

  • web-root:/var/www/html: This will add your site’s static assets, copied to a volume called web-root, to the the /var/www/html directory on the container.
  • ./nginx-conf:/etc/nginx/conf.d: This will bind mount the Nginx configuration directory on the host to the relevant directory on the container, ensuring that any changes you make to files on the host will be reflected in the container.
  • certbot-etc:/etc/letsencrypt: This will mount the relevant Let’s Encrypt certificates and keys for your domain to the appropriate directory on the container.
  • certbot-var:/var/lib/letsencrypt: This mounts Let’s Encrypt’s default working directory to the appropriate directory on the container.

Next, add the configuration options for the certbot container. Be sure to replace the domain and email information with your own domain name and contact email:

~/node_project/docker-compose.yml
...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain  -d www.your_domain 

This definition tells Compose to pull the certbot/certbot image from Docker Hub. It also uses named volumes to share resources with the Nginx container, including the domain certificates and key in certbot-etc, the Let’s Encrypt working directory in certbot-var, and the application code in web-root.

Again, you’ve used depends_on to specify that the certbot container should be started once the webserver service is running.

The command option specifies the command to run when the container is started. It includes the certonly subcommand with the following options:

  • --webroot: This tells Certbot to use the webroot plugin to place files in the webroot folder for authentication.
  • --webroot-path: This specifies the path of the webroot directory.
  • --email: Your preferred email for registration and recovery.
  • --agree-tos: This specifies that you agree to ACME’s Subscriber Agreement.
  • --no-eff-email: This tells Certbot that you do not wish to share your email with the Electronic Frontier Foundation (EFF). Feel free to omit this if you would prefer.
  • --staging: This tells Certbot that you would like to use Let’s Encrypt’s staging environment to obtain test certificates. Using this option allows you to test your configuration options and avoid possible domain request limits. For more information about these limits, please read Let’s Encrypt’s rate limits documentation.
  • -d: This allows you to specify domain names you would like to apply to your request. In this case, you’ve included your_domain and www.your_domain. Be sure to replace these with your own domains.

As a final step, add the volume and network definitions. Be sure to replace the username here with your own non-root user:

~/node_project/docker-compose.yml
...
volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge

Your named volumes include your Certbot certificate and working directory volumes, and the volume for your site’s static assets, web-root. In most cases, the default driver for Docker volumes is the local driver, which on Linux accepts options similar to the mount command. Thanks to this, you are able to specify a list of driver options with driver_opts that mount the views directory on the host, which contains your application’s static assets, to the volume at runtime. The directory contents can then be shared between containers. For more information about the contents of the views directory, please read Step 2 of How To Build a Node.js Application with Docker.

The following is the complete docker-compose.yml file:

~/node_project/docker-compose.yml
version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network

  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --staging -d your_domain  -d www.your_domain 

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge  

With the service definitions in place, you are ready to start the containers and test your certificate requests.

Step 4 — Obtaining SSL Certificates and Credentials

You can start the containers with docker-compose up. This will create and run your containers and services in the order you have specified. Once your domain requests succeed, your certificates will be mounted to the /etc/letsencrypt/live folder on the webserver container.

Create the services with docker-compose up with the -d flag, which will run the nodejs and webserver containers in the background:

  1. docker-compose up -d

Your output will confirm that your services have been created:

Output
Creating nodejs ... done Creating webserver ... done Creating certbot ... done

Use docker-compose ps to check the status of your services:

  1. docker-compose ps

If everything was successful, your nodejs and webserver services will be Up and the certbot container will have exited with a 0 status message:

Output
Name Command State Ports ------------------------------------------------------------------------ certbot certbot certonly --webroot ... Exit 0 nodejs node app.js Up 8080/tcp webserver nginx -g daemon off; Up 0.0.0.0:80->80/tcp

If you notice anything other than Up in the State column for the nodejs and webserver services, or an exit status other than 0 for the certbot container, be sure to check the service logs with the docker-compose logs command. For example, if you wanted to check the Certbot log, you would run:

  1. docker-compose logs certbot

You can now check that your credentials have been mounted to the webserver container with docker-compose exec:

  1. docker-compose exec webserver ls -la /etc/letsencrypt/live

Once your request succeeds, your output will reveal the following:

Output
total 16 drwx------ 3 root root 4096 Dec 23 16:48 . drwxr-xr-x 9 root root 4096 Dec 23 16:48 .. -rw-r--r-- 1 root root 740 Dec 23 16:48 README drwxr-xr-x 2 root root 4096 Dec 23 16:48 your_domain

Now that you know your request will be successful, you can edit the certbot service definition to remove the --staging flag.

Open the docker-compose.yml file:

  1. nano docker-compose.yml

Find the section of the file with the certbot service definition, and replace the --staging flag in the command option with the --force-renewal flag. This will tell Certbot that you want to request a new certificate with the same domains as an existing certificate. The certbot service definition should have the following definitions:

~/node_project/docker-compose.yml
...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email sammy@your_domain --agree-tos --no-eff-email --force-renewal -d your_domain -d www.your_domain
...

When you’re done editing, save and exit the file. You can now run docker-compose up to recreate the certbot container and its relevant volumes. By including the --no-deps option, you’re telling Compose that it can skip starting the webserver service, since it is already running:

  1. docker-compose up --force-recreate --no-deps certbot

The following output indicates that your certificate request was successful:

Output
Recreating certbot ... done Attaching to certbot certbot | Account registered. certbot | Renewing an existing certificate for your_domain and www.your_domain certbot | certbot | Successfully received certificate. certbot | Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem certbot | Key is saved at: /etc/letsencrypt/live/your_domain phd.com/privkey.pem certbot | This certificate expires on 2022-11-03. certbot | These files will be updated when the certificate renews. certbot | NEXT STEPS: certbot | - The certificate will need to be renewed before it expires. Cert bot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setu p for instructions. certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log certbot | certbot | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - certbot | If you like Certbot, please consider supporting our work by: certbot | * Donating to ISRG / Let's Encrypt: https://letsencrypt.org/do nate certbot | * Donating to EFF: https://eff.org/donate-le certbot | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - certbot exited with code 0

With your certificates in place, you can move on to modifying your Nginx configuration to include SSL.

Step 5 — Modifying the Web Server Configuration and Service Definition

Enabling SSL in your Nginx configuration will involve adding an HTTP redirect to HTTPS and specifying your SSL certificate and key locations. It will also involve specifying the Diffie-Hellman group, which you will use for Perfect Forward Secrecy.

Since you are going to recreate the webserver service to include these additions, you can stop it now:

  1. docker-compose stop webserver

Next, create a directory in your current project directory for the Diffie-Hellman key:

  1. mkdir dhparam

Generate your key with the openssl command:

  1. sudo openssl dhparam -out /home/sammy/node_project/dhparam/dhparam-2048.pem 2048

It will take a few moments to generate the key.

To add the relevant Diffie-Hellman and SSL information to your Nginx configuration, first remove the Nginx configuration file you created earlier:

  1. rm nginx-conf/nginx.conf

Open another version of the file:

  1. nano nginx-conf/nginx.conf

Add the following code to the file to redirect HTTP to HTTPS and to add SSL credentials, protocols, and security headers. Remember to replace your_domain with your own domain:

~/node_project/nginx-conf/nginx.conf

server {
        listen 80;
        listen [::]:80;
        server_name your_domain www.your_domain;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name your_domain www.your_domain;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;

        ssl_buffer_size 8k;

        ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

        ssl_protocols TLSv1.2;
        ssl_prefer_server_ciphers on;

        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

        ssl_ecdh_curve secp384r1;
        ssl_session_tickets off;

        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8;

        location / {
                try_files $uri @nodejs;
        }

        location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # enable strict transport security only if you understand the implications
        }

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
}

The HTTP server block specifies the webroot for Certbot renewal requests to the .well-known/acme-challenge directory. It also includes a rewrite directive that directs HTTP requests to the root directory to HTTPS.

The HTTPS server block enables ssl and http2. To read more about how HTTP/2 iterates on HTTP protocols and the benefits it can have for website performance, please read the introduction to How To Set Up Nginx with HTTP/2 Support on Ubuntu 18.04. This block also includes a series of options to ensure that you are using the most up-to-date SSL protocols and ciphers and that OSCP stapling is turned on. OSCP stapling allows you to offer a time-stamped response from your certificate authority during the initial TLS handshake, which can speed up the authentication process.

The block also specifies your SSL and Diffie-Hellman credentials and key locations.

Finally, you’ve moved the proxy pass information to this block, including a location block with a try_files directive, pointing requests to your aliased Node.js application container, and a location block for that alias, which includes security headers that will enable you to get A ratings on things like the SSL Labs and Security Headers server test sites. These headers include X-Frame-Options, X-Content-Type-Options, Referrer Policy, Content-Security-Policy, and X-XSS-Protection. The HTTP Strict Transport Security (HSTS) header is commented out — enable this only if you understand the implications and have assessed its “preload” functionality.

Once you have finished editing, save and close the file.

Before recreating the webserver service, you need to add a few things to the service definition in your docker-compose.yml file, including relevant port information for HTTPS and a Diffie-Hellman volume definition.

Open the file:

  1. nano docker-compose.yml

In the webserver service definition, add the following port mapping and the dhparam named volume:

~/node_project/docker-compose.yml
...
 webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - dhparam:/etc/ssl/certs
    depends_on:
      - nodejs
    networks:
      - app-network

Next, add the dhparam volume to your volumes definitions. Remember to replace the sammy and node_project directories to match yours:

~/node_project/docker-compose.yml
...
volumes:
  ...
  webroot:
  ...
  dhparam:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/dhparam/
      o: bind

Similarly to the web-root volume, the dhparam volume will mount the Diffie-Hellman key stored on the host to the webserver container.

Save and close the file when you are finished editing.

Recreate the webserver service:

  1. docker-compose up -d --force-recreate --no-deps webserver

Check your services with docker-compose ps:

  1. docker-compose ps

The following output indicates that your nodejs and webserver services are running:

Output
Name Command State Ports ---------------------------------------------------------------------------------------------- certbot certbot certonly --webroot ... Exit 0 nodejs node app.js Up 8080/tcp webserver nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp

Finally, you can visit your domain to ensure that everything is working as expected. Navigate your browser to https://your_domain, making sure to substitute your_domain with your own domain name:

Application Landing Page

A lock icon should appear in your browser’s security indicator. If you would like to, you can navigate to the SSL Labs Server Test landing page or the Security Headers server test landing page. The configuration options included should earn your site an A rating on the SSL Labs Server Test. In order to get an A rating on the Security Headers server test, you would have to uncomment the Strict Transport Security (HSTS) header in your nginx-conf/nginx.conf file:

~/node_project/nginx-conf/nginx.conf
location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # enable strict transport security only if you understand the implications
        }

Again, enable this option only if you understand the implications and have assessed its “preload” functionality.

Step 6 — Renewing Certificates

Let’s Encrypt certificates are valid for 90 days. You can set up an automated renewal process to ensure that they do not lapse. One way to do this is to create a job with the cron scheduling utility. You can schedule a cron job using a script that will renew your certificates and reload your Nginx configuration.

Open a script called ssl_renew.sh in your project directory:

  1. nano ssl_renew.sh

Add the following code to the script to renew your certificates and reload your web server configuration:

~/node_project/ssl_renew.sh
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --ansi never"
DOCKER="/usr/bin/docker"

cd /home/sammy/node_project/
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

This script first assigns the docker-compose binary to a variable called COMPOSE, and specifies the --no-ansi option, which will run docker-compose commands without ANSI control characters. It then does the same with the docker binary. Finally, it changes to the ~/node_project directory and runs the following docker-compose commands:

  • docker-compose run: This will start a certbot container and override the command provided in the certbot service definition. Instead of using the certonly subcommand use the renew subcommand, which will renew certificates that are close to expiring. Also included is the --dry-run option to test the script.
  • docker-compose kill: This will send a SIGHUP signal to the webserver container to reload the Nginx configuration.

It then runs docker system prune to remove all unused containers and images.

Close the file when you are finished editing, then make it executable:

  1. chmod +x ssl_renew.sh

Next, open your root crontab file to run the renewal script at a specified interval:

sudo crontab -e 

If this is your first time editing this file, you will be asked to choose an editor:

crontab
no crontab for root - using an empty one
Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- easiest
  3. /usr/bin/vim.basic
  4. /usr/bin/vim.tiny
Choose 1-4 [2]: 
...

At the end of the file, add the following line:

crontab
...
*/5 * * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

This will set the job interval to every five minutes, so you can test whether your renewal request has worked as intended. You have also created a log file, cron.log, to record relevant output from the job.

After five minutes, check cron.log to confirm whether the renewal request has succeeded:

  1. tail -f /var/log/cron.log

After a few moments, the following output signals a successful renewal:

Output
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Killing webserver ... done
Output
… Congratulations, all simulated renewals succeeded: /etc/letsencrypt/live/your_domain/fullchain.pem (success) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Saving debug log to /var/log/letsencrypt/letsencrypt.log Killing webserver ... Killing webserver ... done Deleted Containers: 00cad94050985261e5b377de43e314b30ad0a6a724189753a9a23ec76488fd78 Total reclaimed space: 824.5kB

Exit out by entering CTRL + C in your terminal.

You can now modify the crontab file to set a daily interval. To run the script every day at noon, for example, you could modify the last line of the file like the following:

crontab
...
0 12 * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

You can also remove the --dry-run option from your ssl_renew.sh script:

~/node_project/ssl_renew.sh
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/sammy/node_project/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af

Your cron job will ensure that your Let’s Encrypt certificates don’t lapse by renewing them when they are eligible. You can also set up log rotation with the Logrotate utility to rotate and compress your log files.

Conclusion

You have used containers to set up and run a Node application with an Nginx reverse proxy. You have also secured SSL certificates for your application’s domain and set up a cron job to renew these certificates when necessary.

If you are interested in learning more about Let’s Encrypt plugins, please review our articles on using the Nginx plugin or the standalone plugin.

You can also learn more about Docker Compose with the following resources:

The Compose documentation is also a great resource for learning more about multi-container applications.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products


Tutorial Series: From Containers to Kubernetes with Node.js

In this series, you will build and containerize a Node.js application with a MongoDB database. The series is designed to introduce you to the fundamentals of migrating an application to Kubernetes, including modernizing your app using the 12FA methodology, containerizing it, and deploying it to Kubernetes. The series also includes information on deploying your app with Docker Compose using an Nginx reverse proxy and Let’s Encrypt.

About the authors

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
42 Comments
Leave a comment...

This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Thanks for this wonderful tutorial. Learned a lot from it working on my Laradock setup at https://github.com/Larastudio/lsdock . This post helped me get moving with Certbot. Nginx tweaks and deployment structure are up next.

By the way, if you have environmental variables in your docker-compose.yml the whole setup will not work as cron cannot read these vars properly.

Don’t be like me, don’t forget to set “–force-renewal” in docker-compose.yml (I lost way to much time than I care to admit).

Where? i don’t see this in the article. They do the renewal of ssl with a cron job. please explain.

I believe OP is talking about Step 5, when you add the ‘certbot’ block to docker-compose.yml

You have to scroll horizontally to realize that there’s a “force-renewal” param added :)

References to create xxx in current directory is ambiguous.

Saying “create xxx in the ~/node_project folder” would make for less head scratching of where you mean to locate the next resource.

Thanks for the tutorial, really helpful.

Just a quick question, If I would like to make a change in app.js file because I am using nodejs as a server proxy RESTful APIs what can I do with volume to update everytime I change the data in app.js file?

i think you should rebuild the docker. this should be a part of a ci/cd pipeline, so whenever you push your change to the right branch, the whole process kicks in.

Very helpful tutorial. I am actually trying to add a subdomain in a different server block on nginx. But I get privacy error on chrome. Is there something else todo apart from adding -d subdomain.mywebsite.com? Got this working:

server {
        listen 80;
        listen [::]:80;
# sub domain added here
        server_name ambroise-rabier.fr www.ambroise-rabier.fr analytics.ambroise-rabier.fr;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
# if you only add analytics.ambroise-rabier.fr you get the error
# by adding ambroise-rabier.fr nginx throw a warning but it work
    server_name ambroise-rabier.fr analytics.ambroise-rabier.fr;

    server_tokens off;

    ssl_certificate /etc/letsencrypt/live/ambroise-rabier.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ambroise-rabier.fr/privkey.pem;

    ssl_buffer_size 8k;

    ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

    ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
    ssl_prefer_server_ciphers on;

    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl_ecdh_curve secp384r1;
    ssl_session_tickets off;

    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8;

    root /var/www/html/matomo;

    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

}

Now I get:

2019/06/16 09:23:57 [warn] 67#67: conflicting server name "ambroise-rabier.fr" on 0.0.0.0:443, ignored
nginx: [warn] conflicting server name "ambroise-rabier.fr" on 0.0.0.0:443, ignored
2019/06/16 09:23:57 [warn] 67#67: conflicting server name "ambroise-rabier.fr" on [::]:443, ignored
nginx: [warn] conflicting server name "ambroise-rabier.fr" on [::]:443, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

How will this hack affect my web server? Is there a better way to do it?

Great tutorial! Had some issues that were mostly self-created, non-root user and references to folders that didn’t exist, but otherwise this tutorial allowed me to create an Express.js container that is now running a build for my React front-end and serving it with https. Fantastic work!

What is nodejs in nodejs:8080 in proxy_pass value.

location / {
    proxy_pass http://nodejs:8080;
}

it’s the container_name: nodejs, that she put in the docker-compose.yml

Hi, can you please upload the completed project to github? I can’t seem to get mine to work once I try and enable https

I had this issue and was able to resolve it by allowing HTTPS on port 443 in my firewall settings - hope this helps!

It’s too bad this doesn’t tie in with your previous tutorial in the series (https://www.digitalocean.com/community/tutorials/how-to-scale-a-node-js-application-with-mongodb-on-kubernetes-using-helm), and explain how to secure the application with Kubernetes … any articles on that?

Looks like the webserver crashes if you never had a certificate done.

So the result of this tutorial only works in the context of this tutorial.

I just added a second webserver that serves only http on port 80

My source files change relatively often, so I was wondering how you would recommend running the “docker-compose up” command without it recreating certificates each time while the cronjob still work. This was a fantastic tutorial by the way.

i don’t know, and I wish the author could relate to this. So what i plan to do is: after completing step 5, (real certificate works), i’m going to comment the whole certbot part of the docker-compose. i don’t need it any more, right? so i’ll keep it in comments.

As soon as i port from http to https everything stops working. I have tried it several times with many tutorials however no luck.

This site can’t be reached www.dashboard.sns-hub.com’s server IP address could not be found. Try running Windows Network Diagnostics. DNS_PROBE_FINISHED_NXDOMAIN

certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log certbot | Plugins selected: Authenticator webroot, Installer None certbot | Renewing an existing certificate certbot | Performing the following challenges: certbot | http-01 challenge for www.dashboard.sns-hub.com certbot | Using the webroot path /var/www/html for all unmatched domains. certbot | Waiting for verification… certbot | Challenge failed for domain www.dashboard.sns-hub.com certbot | http-01 challenge for www.dashboard.sns-hub.com certbot | Cleaning up challenges certbot | Some challenges have failed. certbot | IMPORTANT NOTES: certbot | - The following errors were reported by the server: certbot | certbot | Domain: www.dashboard.sns-hub.com certbot | Type: dns certbot | Detail: DNS problem: NXDOMAIN looking up A for certbot | www.dashboard.sns-hub.com - check that a DNS record exists for this certbot | domain certbot exited with code 1

Where do you think the A record for NXDOMAIN is? Because that’s what the error says it’s looking for…

It works! Oh my god. Thank you so much. I followed the tutorial but instead of Node I was using Django the whole time.

Thanks, nice tutorial. Follow up question:

so every time the live renewal cron runs (every 12 hours here in this example) it will kill the nginx service whether certbot renews the certs or not ?

This is certainly unwanted. We do not want to interrupt the accessibility of our websites unless we have to and the certs need renewal…

Any way to only kill nginx service when we actually renew the certs ?

Kathleen Juell
DigitalOcean Employee
DigitalOcean Employee badge
May 28, 2020

Hey @mikeng – using docker-compose kill with the the SIGHUP signal will reload your nginx config. You can check out this blog post for more details. Hope that helps.

Kathleen,

thanks for elaborating on that. Great tutorial, very detailed.

  • M

in my case this command does not restart, it terminates the container…

in my solution I’ve changed SIGHUP into:

docker-compose exec nginx -s reload

see https://docs.nginx.com/nginx/admin-guide/basic-functionality/runtime-control/

Hi folks

This is a very basic question but here it goes: do I need to run the tutorial on the webserver? I have this domain heykidhealth.com registered in AWS Route 53 and the website is a static one under S3, basically a draft. If I have to execute the tutorial on the webserver, it would not be possible due to it is a bucket and I don’t even know if this is allowed by AWS. Anyway, as I assumed that it could be run from any server, I created a very basic one from which I executed the tutorial but everything went wrong. This is the log I’ve got from certbot:

docker-compose logs certbot
Attaching to certbot
certbot      | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot      | Plugins selected: Authenticator webroot, Installer None
certbot      | Obtaining a new certificate
certbot      | Performing the following challenges:
certbot      | http-01 challenge for heykidhealth.com
certbot      | http-01 challenge for www.heykidhealth.com
certbot      | Using the webroot path /var/www/html for all unmatched domains.
certbot      | Waiting for verification...
certbot      | Challenge failed for domain heykidhealth.com
certbot      | Challenge failed for domain www.heykidhealth.com
certbot      | http-01 challenge for heykidhealth.com
certbot      | http-01 challenge for www.heykidhealth.com
certbot      | Cleaning up challenges
certbot      | Some challenges have failed.
certbot      | IMPORTANT NOTES:
certbot      |  - The following errors were reported by the server:
certbot      |
certbot      |    Domain: heykidhealth.com
certbot      |    Type:   unauthorized
certbot      |    Detail: Invalid response from
certbot      |    http://heykidhealth.com/.well-known/acme-challenge/Rd_DMyGetyEegLzXf8yqf_GAbPxzcQSg1nZrRoXi7Y4
certbot      |    [52.219.97.60]: "<html>\n<head><title>404 Not
certbot      |    Found</title></head>\n<body>\n<h1>404 Not
certbot      |    Found</h1>\n<ul>\n<li>Code: NoSuchKey</li>\n<li>Message: The sp"
certbot      |
certbot      |    Domain: www.heykidhealth.com
certbot      |    Type:   unauthorized
certbot      |    Detail: Invalid response from
certbot      |    http://heykidhealth.com/.well-known/acme-challenge/EyjniwRs3ItyPTtzJFR__Z2kB1TY9Do43A_1S9NC6ao
certbot      |    [52.219.100.99]: "<html>\n<head><title>404 Not
certbot      |    Found</title></head>\n<body>\n<h1>404 Not
certbot      |    Found</h1>\n<ul>\n<li>Code: NoSuchKey</li>\n<li>Message: The sp"
certbot      |
certbot      |    To fix these errors, please make sure that your domain name was
certbot      |    entered correctly and the DNS A/AAAA record(s) for that domain
certbot      |    contain(s) the right IP address.

My goal is to create a wildcard Let’s Encrypt certificate and register it on AWS so all my servers could get certified at once, webserver included, mainly API & database servers. For now, I would be very glad if I manage to create one certificate.

Thanks in advance. Renato

Same as me, I’m facing this issue! Somebody please help us.

Hey @SandyCyanSnorkler, how are you doing?

I changed my approach and headed to acme.sh in order to generate my wildcard certificate and it worked just fine. It uses Let’s Encrypt, it is fast and very straight-forward, took less than 1 minute to finish the process to generate the certificate, and it scheduled a revalidation process with cron on my behalf every 60 days.

However, I didn’t use Docker despite it is Docker friendly. If you learn how to do that with Docker, please let me know.

Renato

Awesome Explanation and works as it is

hi, thanks for the solid guide. My problem is ssl_renewal.sh script you suggested, namely ending line: docker system prune -af

It will delete certbot container / image completely from server, because certbot container stops immediately after renewal job/check. Also I noticed that some new duplicate certbot containers can appear (don’t know why?) with cron restarts, so some cleaning is needed. So, how to clean space but avoid of idle certbot image deleting?

This comment has been deleted

    My app stops working after step 5. Basically the nginx configuration for HTTPS keeps my application from working. Can anyone please help me?

    Here’s my nginx configuration: https://github.com/cyrilcabo/alphadevelopment/blob/master/nginx-conf/nginx.conf

    And here’s my docker-compose.yml: https://github.com/cyrilcabo/alphadevelopment/blob/master/docker-compose.yml

    I am getting this error in websever logs:

    [error] 19#19: *1 connect() failed (111: Connection refused) while connecting to upstream, client: XXX.XXX.XX.XXX, server: [domain name], request: “GET /favicon.ico HTTP/2.0”, upstream: “http://XXX.XXX.XX.X:5000/favicon.ico”, host: [domain name], referrer: “https://[domain name]/”

    Any help?

    Bobby Iliev
    Site Moderator
    Site Moderator badge
    August 17, 2020

    Hello,

    You need to check if your backend service is running on port 5000. To do that run this command here:

    1. netstat -plant | grep 5000

    If your backend service is not running on that port, make sure to either start the backend service and configure it to run on port 5000 or update your Nginx configuration so that the proxy pass uses the correct port.

    Regards, Bobby

    Thanks Bobby. I was using --links somewhere to network my containers which is a legacy feature in docker. I saw spent some hours before realizing this was the issue.

    Bobby Iliev
    Site Moderator
    Site Moderator badge
    August 17, 2020

    Hi @kwabenaadudarkwa,

    No problem, happy to hear that you’ve got it all working!

    Regards, Bobby

    Great article! my project has several sub domains. what should be the approach? Do i need separate certificate for each, or they can share the same one? Is it better to keep all of them in one big docker-compose.yml. or to each his own?

    In my webserver I keep getting the following error message: nginx: [emerg] host not found in upstream “my_container_name” in /etc/nginx/conf.d/nginx.conf:58. Can’t get past this error

    Wow, this is a very well written article - thanks a lot Kathleen. I got an nginx reverse proxying my nodejs app with existing certs figured out following this clipping it to my use case. I especially liked:

    • the consistent well expressed low level detail along with logical flow
    • explanations for individual params in .yml including reasons to aid understanding

    It’s rare to find such clear docs that are also easy to read and understand pretty quickly.

    Kathleen, thank you AND your squad for setting me up with a fully-functional https website using Docker, Nginx, and Ubuntu…a stack I have never used in my professional career. You all did a great job laying it all out. I look forward to learning from you all. Thank you.

    Thanks for this amazing tutorial! I’ve been able to understand SSL and set up my website thanks to this. Very clear and well written :thumbsup:

    Wow thanks for the tutorial, it works like a charm!

    Thanks for the article.

    Everything works for me except the certificate renew process.

    I got the following error when running the ssl_renew.sh

    Status: Downloaded newer image for certbot/certbot:latest Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused “process_linux.go:449: container init caused "rootfs_linux.go:58: mounting \"/var/lib/docker/volumes/shortlink_web-root/_data\" to rootfs \"/var/lib/docker/overlay2/03251f56700c992555a649980d0fc9d00b90a07c8e14576174a4f9443c970935/merged\" at \"/var/lib/docker/overlay2/03251f56700c992555a649980d0fc9d00b90a07c8e14576174a4f9443c970935/merged/var/www/html\" caused \"no such file or directory\""”: unknown Deleted Containers: a9cbaf5fe412ba8d5a4926710231b146cf0e130504dfdd8ad8bdc4963921eee9

    Is there any way to utilize Docker Compose so that we can provision NginX + Certbot with the SSL configuration right off the bat?

    In this tutorial, we have to:

    1.) Run Certbot in Staging mode

    2.) If step 1 works, make an edit to the docker-compose.yml and restart the container to replace the --staging flag with --force-renewal and re-issue the certificates.

    3.) If step 2 works, we make edits to the NginX config to faciliate SSL and reference those certificates, and we add the dhparams, as well as updating the docker-compose.yml with SSL port mappings and the dhparams named volume. Finally, we re-build the nginx webserver container with docker-compose.

    I used git so that I can easily switch between all 3 of these steps with minimal effort - I’m still a beginner to docker + nginx, but one of the big benefits of containerizing that I can see is that we could have a ‘one-click’ solution to instantiate a fresh server with SSL without having to manually edit files on the server with nano and stuff like that.

    However, if I go to step 3 without following this step-by-step editing, I get an error.

    On a fresh server, if we are jumping straight into the SSL config and running docker-compose up, there is a problem.

    Certbot depends on Nginx to be up. However, Nginx is looking for the SSL keys that Certbot has created. On a fresh server, certbot did not make them yet. So Nginx errors out, as it is unable to open the keys we have specified in the configuration - as they do not exist yet. Since Nginx is offline, Certbot can not actually issue the SSL keys in the first place - it needs Nginx running with at least the HTTP configuration to be answering at the Domain we are looking to validate.

    So jumping to the “final version” of the application code and just running docker-compose up will not work to fully provision an HTTPS server with one command (I’m just trying to work out if this is actually possible with docker).

    I’m wondering what the best way to automate running through steps 1-2-3 in sequential order is, maybe a shell script? Is there another tool for it?

    I’m still a docker/nginx newbie, so I might have done something wrong here.

    I also met this issue.

    After finishing the tutorial, I did docker-compose down -v and then docker-compose up -d to check it again, then webserver could not start because of this issue

    I am stuck on step 4 can someone help me. Here are the logs I am getting 502 bad gateway

    For certbot container

    p@tp-prod-docker:~/tymepass/src$ docker logs --tail -100 -f certbot
    Saving debug log to /var/log/letsencrypt/letsencrypt.log
    Plugins selected: Authenticator webroot, Installer None
    Requesting a certificate for tymepass.com and www.tymepass.com erforming the following challenges:
    http-01 challenge for tymepass.com
    http-01 challenge for www.tymepass.com
    Using the webroot path /var/www/html for all unmatched domains.
    Waiting for verification…
    Challenge failed for domain tymepass.com
    Challenge failed for domain www.tymepass.com
    http-01 challenge for tymepass.com
    http-01 challenge for www.tymepass.com
    Cleaning up challenges
    Some challenges have failed.
    IMPORTANT NOTES:

    • The following errors were reported by the server:

      Domain: tymepass.com
      Type: unauthorized
      Detail: Invalid response from
      https://tymepass.com/.well-known/acme-challenge/gryvGl4nX0v2bnTddT4O9FsnRAWQ721EAo3_7CsDVIE
      [2400:6180:100:d0::39:4001]: “<html>\r\n<head><title>502 Bad
      Gateway</title></head>\r\n<body
      bgcolor="white">\r\n<center><h1>502 Bad
      Gateway</h1></center>\r\n<hr><cen”

      Domain: www.tymepass.com
      Type: unauthorized
      Detail: Invalid response from https://www.hugedomains.com/
      [2606:4700:20::681a:725]: “\n<!doctype html>\n<html
      lang="en">\n<head>\n\n<meta charset="utf-8">\n<meta
      name="viewport" content="width=device-width, initial-scal”

      To fix these errors, please make sure that your domain name was
      entered correctly and the DNS A/AAAA record(s) for that domain
      contain(s) the right IP address.
      Saving debug log to /var/log/letsencrypt/letsencrypt.log
      Requesting a certificate for tymepass.com and www.tymepass.com
      Performing the following challenges:
      http-01 challenge for tymepass.com
      http-01 challenge for www.tymepass.com
      Using the webroot path /var/www/html for all unmatched domains.
      Waiting for verification…
      Challenge failed for domain www.tymepass.com
      Challenge failed for domain tymepass.com
      http-01 challenge for www.tymepass.com
      http-01 challenge for tymepass.com
      Cleaning up challenges
      Some challenges have failed.
      IMPORTANT NOTES:

    • The following errors were reported by the server:

      Domain: www.tymepass.com
      Type: unauthorized
      Detail: Invalid response from https://www.hugedomains.com/
      [2606:4700:20::681a:625]: “\n<!doctype html>\n<html
      lang="en">\n<head>\n\n<meta charset="utf-8">\n<meta
      name="viewport" content="width=device-width, initial-scal”

      Domain: tymepass.com
      Type: unauthorized
      Detail: Invalid response from
      https://tymepass.com/.well-known/acme-challenge/wMryabAEySXD6Jb6GAE_lwEKIg8jdM1kA3hZUffZx50
      [2400:6180:100:d0::39:4001]: “<html>\r\n<head><title>502 Bad
      Gateway</title></head>\r\n<body
      bgcolor="white">\r\n<center><h1>502 Bad
      Gateway</h1></center>\r\n<hr><cen”

      To fix these errors, please make sure that your domain name was
      entered correctly and the DNS A/AAAA record(s) for that domain
      contain(s) the right IP address.

    **for webserver container **

    tp@tp-prod-docker:~/tymepass/src$ docker logs --tail -100 -f webserver /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh 10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf is not a file or does not exist /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh /docker-entrypoint.sh: Configuration complete; ready for start up

    This wasted so much time. Don´t go this way, as this is only for experienced developers that already work with linux and docker on a daily basis.

    where is a good tutorial then?

    Wow! This article saved a lot of time. Thank you!

    This is great, thank you.

    I notice the nginx configs are set up for ipv6 - should that work out of the box? Mine does not, and I’m not sure how to set that up.

    The Docker docs seems simple enough, though I don’t know how to pick fixed-cidr-v6, nor --subnet which I think is later required to create a network. Whatever I choose I get:

    Error response from daemon: could not find an available, non-overlapping “IPv6” address pool among the defaults to assign to the network

    And in any case, as I understand it giving a container an ipv6 address would mean it is publicly accessible? I don’t want to do that … maybe that’s why ipv6 is disabled by default?

    Thank you - this tutorial is just what I needed!! BUT I am seeing an odd behavior. I am able to access my website via curl but not browser. I have Googled around for the answer and none of the suggested solutions work. What firewall ports should be enabled for this tutorial to work? My firewall was down. I tried enabling it and opening 443 without success…

    Hi @katjuell ,

    Thank you for this great tutorial.

    But I have an issue after running only docker-compose up -d with the final code. Someone had this issue before and had described it in the comment:

    However, if I go to step 3 without following this step-by-step editing, I get an error.

    On a fresh server, if we are jumping straight into the SSL config and running docker-compose up, there is a problem.

    Certbot depends on Nginx to be up. However, Nginx is looking for the SSL keys that Certbot has created. On a fresh server, certbot did not make them yet. So Nginx errors out, as it is unable to open the keys we have specified in the configuration - as they do not exist yet. Since Nginx is offline, Certbot can not actually issue the SSL keys in the first place - it needs Nginx running with at least the HTTP configuration to be answering at the Domain we are looking to validate.

    So jumping to the “final version” of the application code and just running docker-compose up will not work to fully provision an HTTPS server with one command (I’m just trying to work out if this is actually possible with docker).

    I’m wondering what the best way to automate running through steps 1-2-3 in sequential order is, maybe a shell script? Is there another tool for it?

    How to redirect the user only to example.com even it type www.example.com

    hello, I hope you are doing well. I have a problem and I don’t know how can I solve it? " e Authority reported these problems: certbot | Domain: www[.]yahiabecetti[.]net certbot | Type: dns certbot | Detail: DNS problem: NXDOMAIN looking up A for www[.]yahiabecetti[.]net - check that a DNS record exists for this domain certbot | certbot | Domain: yahiabecetti[.]net certbot | Type: dns certbot | Detail: DNS problem: NXDOMAIN looking up A for yahiabecetti[.]net - check that a DNS record exists for this domain certbot | certbot | Hint: The Certificate Authority failed to download the temporary challenge files created by Certbot. Ensure that the listed domains serve their content from the provided --webroot-path/-w and that files created there can be downloaded from the internet. certbot | certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log certbot | Some challenges have failed. certbot | Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details. certbot exited with code 1 " help me, please !

    Bobby Iliev
    Site Moderator
    Site Moderator badge
    December 28, 2021

    Hi there,

    The domain name that you’ve shared here does not seem to be registered.

    You need to have a registered domain name in order for Let’s Encrypt to be able to issue a valid SSL certificate.

    Alternatively, make sure that you do not have any typos in the domain name and if so, try with the correct one.

    Best, Bobby

    Hi, Thank you so much please can you help me with how can I register my domain name and if it’s free?

    Many thanks, Yahia.

    Bobby Iliev
    Site Moderator
    Site Moderator badge
    December 29, 2021

    Hi there Yahia,

    DigitalOcean does not offer Domain Names. But you can use any Domain name provider and purchase a domain name. I personally use Namecheap but there are many other domain providers out there too.

    Alternatively, you could get a free domain name by following the steps here:

    https://devdojo.com/bobbyiliev/how-to-get-a-free-domain-name-for-your-laravel-project

    However this only includes the following domains:

    • .TK
    • .ML
    • .GA
    • .CF
    • .GQ

    Once the domain has been registered you need to point it to your server by following the steps here:

    https://docs.digitalocean.com/tutorials/dns-registrars/#instructions

    Hope that this helps!

    Hi i packed this tutorial in a git repo , you can find it here https://github.com/Kos-M/docker-nginx-letsencrypt Feel free to send any pull request if you see something odd :)

    Hi, thanks a lot for this guide. I am very new to docker and my attempt to implement this tutorial failed at the first docker-compose up -d command The result was a cerbot container with exited (1) status Here is the return of the docker-compose logs cerbot command |

    http-01 challenge for www.jean.fourrier.net certbot | Using the webroot path /var/www/html for all unmatched domains. certbot | Waiting for verification… certbot | Challenge failed for domain jean.fourrier.net certbot | Challenge failed for domain www.jean.fourrier.net certbot | http-01 challenge for jean.fourrier.net certbot | http-01 challenge for www.jean.fourrier.net certbot | certbot | Certbot failed to authenticate some domains (authenticator: webroot). The Certificate Authority reported these problems: certbot | Domain: jean.fourrier.net certbot | Type: unauthorized certbot | Detail: 216.40.34.37: Invalid response from http://jean.fourrier.net/.well-known/acme-challenge/aFgb042gDmvdPbD6DcHC7KgRshSw5TdgHOhrFyELefY: 404 certbot | certbot | Domain: www.jean.fourrier.net certbot | Type: unauthorized certbot | Detail: 216.40.34.37: Invalid response from http://www.jean.fourrier.net/.well-known/acme-challenge/MbdnONPQiZ0owCk8FMFj5iSwkDt_3_A___N4Dwq5ti4: 404 certbot | certbot | Hint: The Certificate Authority failed to download the temporary challenge files created by Certbot. Ensure that the listed domains serve their content from the provided --webroot-path/-w and that files created there can be downloaded from the internet. certbot | certbot | Cleaning up challenges certbot | Some challenges have failed. certbot | Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.

    I must add that the jean.fourrier.net domain is local, that I try this tutorial on a Fedora 37 OS and that my /etc/hosts include 127.0.0.1 jean.fourrier.net www.jean.fourrier.net

    My final goal is to dockerize a Laravel9 vue3 SPA and this tutorial is a first step in learning on my way to this goal.

    I don’t see what I can do? I need help!

    Thank you for the tutorial. It is very informative and one-of-kind article.

    Would you do a tutorial showing how to implement a CI/CD using this docker compose configuration?

    Thank you, again!

    Thank you so much! Very helpful - all worked well 🙏

    Does this article forget to mention that --force-renewal should be removed afterwards again? or is it just me misunderstanding the flag?

    <3

    I think there is an error in the docker-compose after the dh key is added.

      webserver:
       ....
        volumes:
          - dhparam:/etc/ssl/certs
    

    I need to specify the path as:

      webserver:
       ....
        volumes:
          - ./dhparam:/etc/ssl/certs
    

    for it to work

    Try DigitalOcean for free

    Click below to sign up and get $200 of credit to try our products over 60 days!

    Sign up

    Join the Tech Talk
    Success! Thank you! Please check your email for further details.

    Please complete your information!

    Become a contributor for community

    Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

    DigitalOcean Documentation

    Full documentation for every DigitalOcean product.

    Resources for startups and SMBs

    The Wave has everything you need to know about building a business, from raising funding to marketing your product.

    Get our newsletter

    Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

    New accounts only. By submitting your email you agree to our Privacy Policy

    The developer cloud

    Scale up as you grow — whether you're running one virtual machine or ten thousand.

    Get started for free

    Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

    *This promotional offer applies to new accounts only.