Hello again. Let's recap what we've done so far.
- Migrated an existing separate service (Teamspeak3) to Docker. [Part 1]
- Migrated a basic HTTP server stack (HAProxy and Nginx) to Docker. [Part 2]
- Changed that existing web stack so that it was HTTPS-only. [Part 3]
So what's next? Today, I'll go over how I migrated a Node.JS application over to Docker, adding it to the existing web stack, and using Nginx as a reverse proxy to add HTTPS support.
When we left off, we had HAProxy and Nginx containers configured such that we have multiple web containers, each of which are proxied to from HAProxy using TCP Passthrough. In order to have our reverse proxy work properly, we'll have to create a new Docker image for it (based on the one we currently use), and we'll have to add to the HAProxy configuration and docker-compose.yml
file. Of course, we'll also need to have a service to run behind the proxy. I've chosen to use Ghost (the software on which this blog is running) as our example here, but feel free to swap that out for any service you need.
1. Creating the reverse proxy image
For this step, I created a custom Dockerfile
that takes the nginx:alpine
image that we're using for our Nginx containers, adds the program envplate
, then invokes envplate
on the Nginx configuration file before starting Nginx.
Dockerfile
FROM nginx:alpine
RUN apk update && apk add ca-certificates && update-ca-certificates && \
wget -O /usr/local/bin/ep https://github.com/kreuzwerker/envplate/releases/download/v0.0.8/ep-linux && chmod +x /usr/local/bin/ep
COPY nginx-proxy.conf /etc/nginx/nginx.conf
CMD [ "/usr/local/bin/ep", "-v", "/etc/nginx/nginx.conf", "--", "/usr/sbin/nginx", "-g", "daemon off;" ]
You might also notice that the Dockerfile
copys nginx-proxy.conf
to the image directly, rather than us linking the configuration file later using volumes in docker-compose.yml
. We can do this because of the extra step I've taken of adding envplate
to the system. What envplate
does is parses through a file, replacing tokens with the values of environment variables. This works perfectly with Docker, since we can define environment variables in docker-compose.yml
.
1a. nginx-proxy.conf
For the most part, nginx-proxy.conf
is going to look exactly like our existing nginx.conf
file, which we're already using for our existing Nginx containers. Feel free to copy that to wherever you've created your Dockerfile
, and make sure that it's named nginx-proxy.conf
. There is one relatively small change we need to make, though. In the server
section of our config, under the SSL configuration, we're going to replace the line root /usr/share/nginx/html;
with the following location
section.
nginx-proxy.conf
(excerpt)
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://${APP_HOST:-app}:${APP_HOST_PORT:-8080};
proxy_read_timeout 90;
}
The proxy_set_header
lines are just setting headers that the proxied service can look for so it knows that the requests are being proxied. With our existing setup, the X-Real-IP and X-Forwarded-For headers will always show the IP address of our HAProxy container, as there is no way for us to add in the proper IP address from the remote computer while using TCP Passthrough.
The proxy_pass
line is the most important part of the configuration. It is instructing Nginx to proxy all requests through to the host specified in the environment variable APP_HOST
, using the port specified in the environment variable APP_HOST_PORT
, and also specifies a default host of app
and port of 8080
.
1b. Building the Docker container
Once we've created the Dockerfile
and the nginx-proxy.conf
file, we can build our image. In order to do that, as shown before with the Teamspeak3 image we built, you simply need to run docker build -t [username]/nginx-proxy:v1 .
from within the folder that you created the configuration files. This will create a Docker image that we can now use in our docker-compose.yml
file.
2. docker-compose.yml
Our next step is to edit docker-compose.yml
to create our Ghost instance and an instance of our new Nginx proxy image. For this, we will need to add two new services to our docker-compose.yml
file, and add one more link to the existing HAProxy service, as shown below.
docker-compose.yml
ghost:
image: zzrot/alpine-ghost
environment:
PROD_DOMAIN: https://blog.akpwebdesign.com/
NODE_ENV: production
volumes:
- /home/austin/websites/blog.akpwebdesign.com/content/:/var/lib/ghost/ # This volume is where Ghost will store things like your themes and image uploads.
expose:
- 2368
blog.akpwebdesign.com:
image: [username]/nginx-proxy:v1
restart: always
environment:
- APP_HOST=ghost # These environment variables will be swapped into our nginx-proxy.conf file by envplate.
- APP_HOST_PORT=2368
expose:
- 443
volumes:
- /home/austin/websites/dhparam.pem:/etc/nginx/dhparam.pem:ro
- /etc/letsencrypt/live/blog.akpwebdesign.com/:/usr/share/nginx/ssl/
- /etc/letsencrypt/archive/blog.akpwebdesign.com/:/usr/share/archive/blog.akpwebdesign.com/
links:
- ghost:ghost
haproxy:
image: haproxy:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /srv/websites/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- /srv/websites/haproxy/errors/:/usr/local/etc/haproxy/errors/:ro
- /dev/log:/dev/log
links:
- akpwebdesign.com
- customersite.com:cust01
- blog.akpwebdesign.com:blog # This is the link we added to our HAProxy service.
I've used an existing Ghost image I found, zzrot/alpine-ghost
, which I've found to be a nice, clean Ghost image, ready to go out of the box. You can see the environment variables listed in the environment
section of our Ghost container configuration. As an aside, you can actually use this reverse proxy configuration to proxy anything, simply by filling these values out correctly. For instance, if you need to proxy requests to an external server, simply make APP_HOST
the URL of the server you need, and APP_HOST_PORT
port 80 (or whatever other port your external service is running on).
2a. Let's Encrypt
Don't forget that we still need to generate a LetsEncrypt SSL certificate for our domain or subdomain. This can be done in the same fashion as the last tutorial, and as long as you've been following along, the certificate should be ready to go in the proper location.
3. haproxy.cfg
Now we only have one thing left to do, and that's to edit our haproxy.cfg
file to include the new subdomain and backend configuration. For this, we'll need to add a new redirect
line to the www
frontend, a new use_backend
line to the ssl
frontend, and of course, a new backend to point to our Nginx proxy server (which we've aliased to blog
in the docker-compose.yml
file above).
haproxy.cfg
(excerpts)
[...]
#######################
# Non-Secure Frontend #
#######################
frontend www
bind 0.0.0.0:80
option http-server-close
option forwardfor
# Redirect AKP Web Design sites to SSL.
redirect scheme https code 301 if { hdr(host) -i akpwebdesign.com } !{ ssl_fc } !{ path_beg /.well-known/acme-challenge }
redirect scheme https code 301 if { hdr(host) -i blog.akpwebdesign.com } !{ ssl_fc } !{ path_beg /.well-known/acme-challenge } # New redirect for blog.
# Redirect customer sites to SSL.
redirect scheme https code 301 if { hdr(host) -i customersite.com } !{ ssl_fc } !{ path_beg /.well-known/acme-challenge }
# LetsEncrypt backend, no SSL.
use_backend le if { path_beg /.well-known/acme-challenge }
# Default backend.
default_backend le
################
# SSL Frontend #
################
frontend ssl
mode tcp
option tcplog
bind 0.0.0.0:443
option socket-stats
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
# AKP Web Design sites
use_backend akp if { req_ssl_sni -i akpwebdesign.com }
use_backend blog if { req_ssl_sni -i blog.akpwebdesign.com } # New use_backend for blog.
# Customer sites
use_backend customer if { req_ssl_sni -i customersite.com }
# No default_backend, as certs would be broken anyway.
# default_backend null
[...]
#########################
# blog.akpwebdesign.com #
#########################
backend blog
mode tcp
# maximum SSL session ID length is 32 bytes.
stick-table type binary len 32 size 30k expire 30m
acl clienthello req_ssl_hello_type 1
acl serverhello rep_ssl_hello_type 2
# use tcp content accepts to detects ssl client and server hello.
tcp-request inspect-delay 5s
tcp-request content accept if clienthello
# no timeout on response inspect delay by default.
tcp-response content accept if serverhello
stick on payload_lv(43,1) if clienthello
# Learn on response if server hello.
stick store-response payload_lv(43,1) if serverhello
option ssl-hello-chk
server web1 blog:443
5. Starting our new containers
This is actually the easiest step. Make sure you're in the folder where your docker-compose.yml
file is located, and run docker-compose up -d
. This will start any new containers needed, and restart the HAProxy container once all the other containers are running. This command will also restart any containers that need to be updated, so you can use this if you have changed your configuration files, so you don't need to bring your entire stack down if you've only changed one container.
That's the end of this tutorial. Once your containers are all up and running, you should be able to navigate to the URL you've specified above (assuming you've already got DNS set up for it), and see your fresh new Ghost installation!