User Tools

Site Tools


Secure Nginx in Docker


Docker and the official Nginx image make development and administration of public sites much easier, but there's still many sites that don't use encryption. The fact that my blog has better encryption than many large, commercial businesses is a sad commentary on where we stand.

Google penalizes sites for not having proper encryption, and in $theCurrentYear, there's really not a reason why you shouldn't, especially considering that it's available for free via an E.F.F. Project (as in speech, not as in beer - please feel free to donate to Let's Encrypt).


Setup source


Create source directory, and clone H5BP Nginx configuration repository:

mkdir -pv ~/src;

git clone \ \
    ~/src/server-configs-nginx \

Site template

Download template.conf to your ~/src/ directory:

wget \
    -O ~/src/template.conf \
# Based upon:
# Choose between www and non-www, listen on the *wrong* one and redirect to
# the right one --
server {
	listen [::]:80;
	listen 80;
	# listen on both hosts
	server_name %HOSTNAME% www.%HOSTNAME%;
	# and redirect to the https host (declared below)
	# avoiding http://www -> https://www -> https:// chain.
	return 301 https://%HOSTNAME%$request_uri;
# https://www.%HOSTNAME%/
server {
	listen [::]:443 ssl http2;
	listen 443 ssl http2;
	# listen on the wrong host
	server_name www.%HOSTNAME%;
	include h5bp/directive-only/ssl.conf;
	ssl_certificate /etc/letsencrypt/live/%HOSTNAME%/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/%HOSTNAME%/privkey.pem;
	# and redirect to the non-www host (declared below)
	return 301 https://%HOSTNAME%$request_uri;
# https://%HOSTNAME%/
server {
    location / {
        root   /sites/%HOSTNAME%/public;
        index  index.html index.htm;
	listen [::]:443 ssl http2;
	listen 443 ssl http2;
	# The host name to respond to
	server_name %HOSTNAME%;
	access_log	/var/log/nginx/%HOSTNAME%/access.log	main;
	error_log	/var/log/nginx/%HOSTNAME%/error.log	warn;
	include h5bp/directive-only/ssl.conf;
	# Path for static files
	root /sites/%HOSTNAME%/public;
	# Specify a charset
	charset utf-8;
	# Custom 404 page
	error_page 404 /404.html;
	# Include the basic h5bp config set
	include h5bp/basic.conf;
	ssl_certificate /etc/letsencrypt/live/%HOSTNAME%/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/%HOSTNAME%/privkey.pem;
2017/09/14 01:20 · Administrator

Create Nginx paths

mkdir -pv \
    ~/srv/nginx/ \
    ~/srv/nginx/log/ \
    ~/srv/sites/ \

Initialize Nginx

    docker run --name tmp-nginx-container -d nginx:latest

    docker cp tmp-nginx-container:/etc/nginx/ ~/srv/nginx/

    docker cp tmp-nginx-container:/usr/share/nginx/html/ ~/srv/sites/default/

    mv -iv ~/srv/nginx/nginx ~/srv/nginx/etc

    docker rm -f tmp-nginx-container

    mkdir -pv \
        ~/srv/nginx/etc/sites-available \
        ~/srv/nginx/etc/sites-enabled \

Setup a fall-through site for sites-available, and then enable it:

mv -iv \
    ~/srv/nginx/etc/conf.d/default.conf \
    ~/srv/nginx/etc/sites-available/default \

cd ~/srv/nginx/etc/sites-enabled/
ln -sv \
    ../sites-available/default \
    ./ \

Add default_server parameter to ~/srv/nginx/etc/sites-available/default. The parameter should now look like:

server {
    listen       80 default_server;
    server_name  localhost;

H5BP configuration


cp -vaR \
    ~/src/server-configs-nginx/h5bp \
    ~/srv/nginx/etc/ \

cp -vaRf \
    ~/src/server-configs-nginx/mime.types \
    ~/srv/nginx/etc/ \

cp -vaRf \
    ~/src/server-configs-nginx/nginx.conf \
    ~/srv/nginx/etc/ \


Open ~/srv/nginx/etc/nginx.conf and set the following variables (most already exist in the configuration, and simply need to be updated)

user nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
worker_connections  1024;
access_log  /var/log/nginx/access.log  main;
keepalive_timeout  65;

While editing this file, comment out the TCP push option:

#tcp_nopush     on;



read -p "Website FQDN : " fqdnSite


cd ~/srv/nginx/etc/sites-available/
sed "s/%HOSTNAME%/${fqdnSite}/g" < ~/src/template.conf > "${fqdnSite}"


mkdir -pv \
    ~/srv/sites/${fqdnSite}/public/ \
    ~/srv/nginx/log/${fqdnSite}/ \


echo "Content for ${fqdnSite} goes here." > "${HOME}/srv/sites/${fqdnSite}/public/index.html"


cd ~/srv/nginx/etc/sites-enabled/
ln -sv ../sites-available/${fqdnSite}

Let's Encrypt C.A.

The subsequent Docker run command is geared towards your TLS Certificate coming from the “Let's Encrypt” Certificate Authority. If you are using a different provider, you will need to modify the Site template above, and Nginx start command below, for the certificate and key files to point to the appropriate location.

Assuming that you don't have one, please follow the subsequent steps in this section.

Certificate Request


IMHO, DNS authorization via a TXT record is one of the most easy methodologies for validating domain ownership. I always recommend FreeDNS, but there's many other providers (e.g. Digital Ocean, Google Cloud DNS, and Amazon Route 53) and APIs to control varying services (e.g. DNSControl).

docker run -it --rm --name certbot \
    -v ~/srv/letsencrypt/etc:/etc/letsencrypt \
    -v ~/srv/letsencrypt/var/lib:/var/lib/letsencrypt \
    -v ~/srv/letsencrypt/var/log:/var/log/letsencrypt \
    certbot/certbot \
    -d "${fqdnSite}" \
    -d "www.${fqdnSite}" \
    -d "dev.${fqdnSite}" \
    -d "dev.www.${fqdnSite}" \
    --manual --preferred-challenges dns certonly

Certificate Renewal

Certificates issued by Let's Encrypt expire after 3 months. To avoid certificate expiration errors being displayed to your audience, you can use Cron to automatically run the following command:

docker run -it --rm --name certbot \
    -v ~/srv/letsencrypt/etc:/etc/letsencrypt \
    -v ~/srv/letsencrypt/var/lib:/var/lib/letsencrypt \
    -v ~/srv/letsencrypt/var/log:/var/log/letsencrypt \
    certbot/certbot \



Let's run Nginx, and view the default welcome page.

docker run \
    --name webserver \
    --volume ~/srv/nginx/etc/:/etc/nginx/:ro \
    --volume ~/srv/nginx/log/:/var/log/nginx/ \
    --volume ~/srv/letsencrypt/etc:/etc/letsencrypt/:ro \
    --volume ~/srv/sites/default/:/usr/share/nginx/html/:ro \
    --volume ~/srv/sites/:/sites/:ro \
    --publish 80:80 \
    --publish 443:443 \
    -d \
    --restart=always \
    nginx:latest \


To reflect our new site additions, we'll need to restart Nginx.

docker restart webserver


There's discussion about upgrading, and the steps are simple:

  1. Pull the latest version:
    docker pull nginx:latest
  2. Stop the web server's container:
    docker stop webserver
  3. Remove the web server's container:
    docker rm webserver
  4. Re-run the web server using the Nginx run command above.
  5. Clean-up:
    docker rm $(docker ps --all --quiet --no-trunc --filter 'status=exited') && \
        docker rmi $(docker images --quiet --filter 'dangling=true')


In subsequent guides that I'll write in the future, I will demonstrate how to:

  1. Use this guide as a base for isolating domains and/or paths as Docker containers from each other, and how to run those sites built in PHP, Python, Ruby, or as static sites.
  2. Develop in a test environment that can be “hot-swapped”.
  3. Load-balancing with:
    • HAProxy
    • Nginx load balancer
    • Kubernetes
2017/07/03/secure-nginx-in-docker.txt · Last modified: 2017/12/31 00:32 by Administrator