Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enable HTTPS on AWS EC2 running an NGINX Docker container?

I have an EC2 instance on AWS that runs Amazon Linux 2.

On it, I installed Git, docker, and docker-compose. Once done, I cloned my repository and ran docker-compose up to get my production environment up. I go to the public DNS, and it works.

I now want to enable HTTPS onto the site.

My project has a frontend using React to run on an Nginx-alpine server. The backend is a NodeJS server.

This is my nginx.conf file:

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri /index.html;
    }

    location /api/ {
        proxy_pass http://${PROJECT_NAME}_backend:${NODE_PORT}/;
    }    

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Here's my docker-compose.yml file:

version: "3.7"
services:
##############################
# Back-End Container
##############################
  backend: # Node-Express backend that acts as an API.
    container_name: ${PROJECT_NAME}_backend
    init: true
    build:
      context: ./backend/
      target: production
    restart: always
    environment:
      - NODE_PATH=${EXPRESS_NODE_PATH}
      - AWS_REGION=${AWS_REGION}
      - NODE_ENV=production
      - DOCKER_BUILDKIT=1
      - PORT=${NODE_PORT}
    networks:
      - client
##############################
# Front-End Container
##############################
  nginx:
    container_name: ${PROJECT_NAME}_frontend
    build:
      context: ./frontend/
      target: production
      args:
        - NODE_PATH=${REACT_NODE_PATH}
        - SASS_PATH=${SASS_PATH}
    restart: always
    environment:
      - PROJECT_NAME=${PROJECT_NAME}
      - NODE_PORT=${NODE_PORT}
      - DOCKER_BUILDKIT=1
    command: /bin/ash -c "envsubst '$$PROJECT_NAME $$NODE_PORT' < /etc/nginx/conf.d/nginx.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
    expose:
      - "80"
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - client
##############################
# General Config
##############################
networks:
  client:

I know there's a Docker image for certbot, but I'm not sure how to use it. I'm also worried about the way I'm proxying requests to /api/ to the server over http. Will that also give me any problems?


Edit:

Attempt #1: Traefik

I created a Traefik container to route all traffic through HTTPS.

version: '2'

services:
  traefik:
    image: traefik
    restart: always
    ports:
      - 80:80
      - 443:443
    networks:
      - web
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/traefik/traefik.toml:/traefik.toml
      - /opt/traefik/acme.json:/acme.json
    container_name: traefik

networks:
  web:
    external: true

For the toml file, I added the following:

debug = false

logLevel = "ERROR"
defaultEntryPoints = ["https","http"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  [entryPoints.https.tls]

[retry]

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "ec2-00-000-000-00.eu-west-1.compute.amazonaws.com"
watch = true
exposedByDefault = false

[acme]
storage = "acme.json"
entryPoint = "https"
onHostRule = true
[acme.httpChallenge]
entryPoint = "http"

I added this to my docker-compose production file:

labels:
  - "traefik.docker.network=web"
  - "traefik.enable=true"
  - "traefik.basic.frontend.rule=Host:ec2-00-000-000-00.eu-west-1.compute.amazonaws.com"
  - "traefik.basic.port=80"
  - "traefik.basic.protocol=https"

I ran docker-compose up for the Traefik container, and then ran docker-compose up on my production image. I got the following error:

unable to obtain acme certificate

I'm reading the Traefik docs and apparently there's a way to configure the toml file specifically for Amazon ECS: https://docs.traefik.io/configuration/backends/ecs/

Am I on the right track?

like image 311
yaserso Avatar asked Sep 05 '25 20:09

yaserso


2 Answers

Enabling SSL is done through following the tutorial on Nginx and Let's Encrypt with Docker in Less Than 5 Minutes. I ran into some issues while following it, so I will try to clarify some things here.

The steps include adding the following to the docker-compose.yml:

##############################
# Certbot Container
##############################
  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./frontend/data/certbot/conf:/etc/letsencrypt
      - ./frontend/data/certbot/www:/var/www/certbot

As for the Nginx Container section of the docker-compose.yml, it should be amended to include the same volumes added to the Certbot Container, as well as add the ports and expose configurations:

  service_name:
    container_name: container_name
    image: nginx:alpine
    command: /bin/ash -c "exec nginx -g 'daemon off;'"
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    expose:
      - "80"
      - "443"
    ports:
      - "80:80"
      - "443:443"
    networks:
      - default

The data folder may be saved anywhere else, but make sure to know where it is and make sure to reference it properly when reused later. In this example, I am simply saving it in the same directory as the docker-compose.yml file.

Once the above configurations are put into place, a couple of steps are to be taken in order to initialize the issuance of the certificates.

Firstly, your Nginx configuration (default.conf) is to be changed to accommodate the domain verification request:

server {
    listen       80;
    server_name example.com www.example.com;
    server_tokens off;

    location / {
        return 301 https://$server_name$request_uri;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
}

server {
     listen 443 ssl;
     server_name example.com www.example.com;
     server_tokens off;

     ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
     include /etc/letsencrypt/options-ssl-nginx.conf;
     ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

     location / {
         root   /usr/share/nginx/html;
         index  index.html index.htm;
         try_files $uri /index.html;
         proxy_set_header    Host                $http_host;
         proxy_set_header    X-Real-IP           $remote_addr;
         proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
     }
}

Once the Nginx configuration file is amended, a dummy certificate is created to allow for Let's Encrypt validation to take place. There is a script that does all of this automatically, which can be downloaded, into the root of the project, using CURL, before being amended to suit the environment. The script would also need to be made executable using the chmod command:

curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh && chmod +x init-letsencrypt.sh

Once the script is downloaded, it is to be amended as follows:

#!/bin/bash

if ! [ -x "$(command -v docker-compose)" ]; then
  echo 'Error: docker-compose is not installed.' >&2
  exit 1
fi

-domains=(example.org www.example.org)
+domains=(example.com www.example.com)
rsa_key_size=4096
-data_path="./data/certbot"
+data_path="./data/certbot"
-email="" # Adding a valid address is strongly recommended
+email="[email protected]" # Adding a valid address is strongly recommended
staging=0 # Set to 1 when testing setup to avoid hitting request limits

if [ -d "$data_path" ]; then
  read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
  if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
    exit
  fi
fi

if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
  echo "### Downloading recommended TLS parameters ..."
  mkdir -p "$data_path/conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
  echo
fi

echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
-docker-compose run --rm --entrypoint "\
+docker-compose -f docker-compose.yml run --rm --entrypoint "\
  openssl req -x509 -nodes -newkey rsa:1024 -days 1\
    -keyout '$path/privkey.pem' \
    -out '$path/fullchain.pem' \
    -subj '/CN=localhost'" certbot
echo

echo "### Starting nginx ..."
-docker-compose up --force-recreate -d nginx
+docker-compose -f docker-compose.yml up --force-recreate -d service_name
echo

echo "### Deleting dummy certificate for $domains ..."
-docker-compose run --rm --entrypoint "\
+docker-compose -f docker-compose.yml run --rm --entrypoint "\
  rm -Rf /etc/letsencrypt/live/$domains && \
  rm -Rf /etc/letsencrypt/archive/$domains && \
  rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo

echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
  domain_args="$domain_args -d $domain"
done

# Select appropriate email arg
case "$email" in
  "") email_arg="--register-unsafely-without-email" ;;
  *) email_arg="--email $email" ;;
esac

# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi

-docker-compose run --rm --entrypoint "\
+docker-compose -f docker-compose.yml run --rm --entrypoint "\
  certbot certonly --webroot -w /var/www/certbot \
    $staging_arg \
    $email_arg \
    $domain_args \
    --rsa-key-size $rsa_key_size \
    --agree-tos \
    --force-renewal" certbot
echo

echo "### Reloading nginx ..."
-docker-compose exec nginx nginx -s reload
+docker-compose exec service_name nginx -s reload

I have made sure to always include the -f flag with the docker-compose command just in case someone doesn't know what to change if they had a custom named docker-compose.yml file. I have also made sure to set the service name as service_name to make sure to differentiate between the service name and the Nginx command, unlike the tutorial.

Note: If unsure about the fact that the setup is working, make sure to set staging as 1 to avoid hitting request limits. It is important to remember to set it back to 0 once testing is done and redo all steps from amending the init-letsencrypt.sh file. Once testing is done and the staging is set to 0, it is important to stop previous running containers and delete the data folder for the proper initial certification to ensue:

$ docker-compose -f docker-compose.yml down && yes | docker system prune -a --volumes && sudo rm -rf ./data

Once the certificates are ready to be initialized, the script is to be run using sudo; it is very important to use sudo, as issues will occur with the permissions inside the containers if run without it.

$ sudo ./init-letsencrypt.sh

After the certificate is issued, there is the matter of automatically renewing the certificate; two things need to be done:

  • In the Nginx Container, Nginx would reload the newly obtained certificates through the following ammendment:
service_name:
...
- command: /bin/ash -c "exec nginx -g 'daemon off;'"
+ command: /bin/ash -c "while :; do sleep 6h & wait $${!}; nginx -s reload; done & exec nginx -g 'daemon off;'"
...
  • In the Certbot Container section, the following is to be add to check if the certificate is up for renewal every twelve hours, as recommended by Let's Encrypt:
certbot:
...
+ entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"

Before running docker-compose -f docker-compose.yml up, the ownership of the data should be changed folder to the ec2-user; this is to avoid running into permission errors when running docker-compose -f docker-compose.yml up, or running it in sudo mode:

sudo chown ec2-user:ec2-user -R /path/to/data/

Don't forget to add a CAA record in your DNS provider for Let's Encrypt. You may read here for more information on how to do so.

If you run into any issues with the Nginx container because you are substituting variables and $server_name and $request_uri are not appearing properly, you may refer to this issue.

like image 153
yaserso Avatar answered Sep 08 '25 17:09

yaserso


Easiest way would be to setup a ALB and use it for HTTPS.

  1. Create ALB
  2. Add 443 Listener to ALB
  3. Generate Certificate using AWS Certificate Manager
  4. Set the Certificate to the default cert for the load balancer
  5. Create Target Group
  6. Add your EC2 Instance to the Target Group
  7. Point the ALB to the Target Group

Requests will be served using the ALB with https

like image 28
D. Vinson Avatar answered Sep 08 '25 15:09

D. Vinson