Skip to content

Instantly share code, notes, and snippets.

@igortrinidad
Created December 16, 2020 00:56
Show Gist options
  • Save igortrinidad/c0293f2a79ce55c9abe32cf6da2cb2ed to your computer and use it in GitHub Desktop.
Save igortrinidad/c0293f2a79ce55c9abe32cf6da2cb2ed to your computer and use it in GitHub Desktop.
CEE Docker setup with postgres, redis, nginx e certbot
# /docker-volumes/nginx/app.conf
server {
listen 80;
server_name cee.igortrindade.dev;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name cee.igortrindade.dev;
root /home/cee/api/public;
ssl_certificate /etc/letsencrypt/live/cee.igortrindade.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cee.igortrindade.dev/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
client_max_body_size 9M;
charset utf-8;
access_log off;
error_log /var/log/nginx/cee.igortrindade.dev.log error;
location / {
proxy_pass http://cee_app:3333;
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;
}
location ~ /\.(?!well-known).* {
deny all;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|mp3|svg)$ {
expires 365d;
add_header Cache-Control "public, no-transform";
}
}
version: "3.4"
services:
app:
build: .
container_name: cee_app
ports:
- "3333:3333"
environment:
HOST: '0.0.0.0'
DB_HOST: db
REDIS_HOST: redis
volumes:
- .:/app
networks:
- network_cee
db:
container_name: cee_pg
image: postgres
restart: unless-stopped
ports:
- "5432:5432"
volumes:
- ./docker-volumes/postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: cee
POSTGRES_PASSWORD: 123456
networks:
- network_cee
redis:
container_name: cee_redis
image: redis
restart: unless-stopped
ports:
- "6379:6379"
networks:
- network_cee
nginx:
container_name: cee_nginx
image: nginx
restart: unless-stopped
volumes:
- ./docker-volumes/nginx:/etc/nginx/conf.d
- ./docker-volumes/certbot/conf:/etc/letsencrypt
- ./docker-volumes/certbot/www:/var/www/certbot
- ./docker-volumes/log/nginx:/var/log/nginx
ports:
- "80:80"
- "443:443"
environment:
HOST: '0.0.0.0'
networks:
- network_cee
depends_on:
- app
- db
- redis
- certbot
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3333"]
interval: 30s
timeout: 20s
retries: 10
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
certbot:
container_name: cee_certbot
image: certbot/certbot
restart: unless-stopped
volumes:
- ./docker-volumes/certbot/conf:/etc/letsencrypt
- ./docker-volumes/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
network_cee:
FROM node:13.7.0
USER root
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
#!/bin/bash
if ! [ -x "$(command -v docker-compose)" ]; then
echo 'Error: docker-compose is not installed.' >&2
exit 1
fi
domains=(cee.igortrindade.dev www.cee.igortrindade.dev)
rsa_key_size=4096
data_path="./docker-volumes/certbot"
email="" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your 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/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/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 "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -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
echo
echo "### Deleting dummy certificate for $domains ..."
docker-compose 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 "\
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
The other day, I wanted to quickly launch an nginx server with Let’s Encrypt certificates. I expected the task to be easy and straightforward. Turns out: I was wrong, it took a significant amount of time and it’s quite a bit more complicated.
Of course, in the grand scheme of things, it is pretty straightforward. But there are a couple of details you need to be aware of. The goal of this guide is to help you build a docker-compose setup that runs nginx in one container and a service for obtaining and renewing HTTPS certificates in another. Whether you’re using nginx as a proxy for your web app or just for serving static files, this guide is for you.
TL;DR: The full code from this guide is available on GitHub.
Quick Reminder: What is docker-compose?
docker-compose is a tool for defining containers and running them. It’s a great choice when you have multiple interdependent containers but you don’t need a full-blown container cluster like Kubernetes.
The official documentation puts it like this:
With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration
This guide requires docker-compose. If you don’t have it yet, take a look at the installation instructions and get it.
Hint: If you’re installing docker-compose on CoreOS, it needs to go into /opt/bin instead of /usr/local/bin.
The Setup
Official images of nginx and an automated build of certbot, the EFF’s tool for obtaining Let’s Encrypt certificates, are available in the Docker library.
Let’s begin with a basic docker-compose.yml configuration file that defines containers for both images:
version: '3'
services:
nginx:
image: nginx:1.15-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./data/nginx:/etc/nginx/conf.d
certbot:
image: certbot/certbot
Here is a simple nginx configuration that redirects all requests to HTTPS. The second server definition sets up a proxy to example.org for demonstration purposes. This is where you would add your own configuration for proxying requests to your app or serving local files.
Save this file as data/nginx/app.conf alongside docker-compose.yml. Change example.org in both occurrences of server_name to your domain name.
server {
listen 80;
server_name example.org;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name example.org;
location / {
proxy_pass http://example.org; #for demo purposes
}
}
If you would try to run docker-compose up now, nginx would fail to start because there is no certificate. We need to make some adjustments.
Linking up nginx and certbot
Let’s Encrypt performs domain validation by requesting a well-known URL from a domain. If it receives a certain response (the “challenge”), the domain is considered validated. This is similar to how Google Search Console establishes ownership of a website. The response data is provided by certbot, so we need a way for the nginx container to serve files from certbot.
First of all, we need two shared Docker volumes. One for the validation challenges, the other for the actual certificates.
Add this to the volumes list of the nginx section in docker-compose.yml.
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
And this is the counterpart that needs to go in the certbot section:
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
Now we can make nginx serve the challenge files from certbot! Add this to the first (port 80) section of our nginx configuration (data/nginx/app.conf):
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
After that, we need to reference the HTTPS certificates. Add the soon-to-be-created certificate and its private key to the second server section (port 443). Make sure to once again replace example.org with your domain name.
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
And while we’re at it: The folks at Let’s Encrypt maintain best-practice HTTPS configurations for nginx. Let’s also add them to our config file. This will score you a straight A in the SSL Labs test!
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
The Chicken or the Egg?
Now for the tricky part. We need nginx to perform the Let’s Encrypt validation But nginx won’t start if the certificates are missing.
So what do we do? Create a dummy certificate, start nginx, delete the dummy and request the real certificates.
Luckily, you don’t have to do all this manually, I have created a convenient script for this.
Download the script to your working directory as init-letsencrypt.sh:
curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh
Edit the script to add in your domain(s) and your email address. If you’ve changed the directories of the shared Docker volumes, make sure you also adjust the data_path variable as well.
Then run chmod +x init-letsencrypt.sh and sudo ./init-letsencrypt.sh.
Automatic Certificate Renewal
Last but not least, we need to make sure our certificate is renewed when it’s about to expire. The certbot image doesn’t do that automatically but we can change that!
Add the following to the certbot section of docker-compose.yml:
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
This will check if your certificate is up for renewal every 12 hours as recommended by Let’s Encrypt.
In the nginx section, you need to make sure that nginx reloads the newly obtained certificates:
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
This makes nginx reload its configuration (and certificates) every six hours in the background and launches nginx in the foreground.
Docker-compose Me Up!
Everything is in place now. The initial certificates have been obtained and our containers are ready to launch. Simply run docker-compose up and enjoy your HTTPS-secured website or app.
Did you find this guide helpful? Make sure to subscribe to my newsletter so you don’t miss the next article with useful deployment tips!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment