Migrating Existing Services to Docker, Part Two

In the last post, I talked about how to migrate a Teamspeak 3 server over to a Docker container. This time around, we're going to get a little more advanced, as we migrate multiple web servers over using docker-compose to piece everything together.

Docker-compose is a tool that allows you to start up and link together multiple Docker containers, all at one time. It's very useful for starting up a stack of services that rely on one another, such as your web server(s) and load balancer. Docker-compose can also be used to create and maintain very complex service stacks, with many different types of services and containers. Let's jump right in.

1. The Audit (continued)

If you remember from last time, I had audited the servers I was running to see what I needed to migrate, keeping track in a text file that looked something like this:

warwick:  
  - akpwebdesign.com website
  - [customer] website
  ✓ Teamspeak 3
  ...
leblanc:  
  - HAProxy
  - nothing else?? really?
AKP48:  
  - AKP48 (Node.js)
  - Bravify version tracker (Node.js)
  - LoL static data (Node.js)
  ...
irc:  
  - weechat
  - znc

Today, we're going to focus on two things: The websites running on warwick, and the HAProxy instance running on leblanc.

2. Getting started with docker-compose

Once I had docker-compose installed, I went ahead and created a folder to stage my files in, and got to work. In order to start with docker-compose, you need a docker-compose.yml file, which is where you define your services and links. I used version 2 of the docker-compose format, but version 1 is still supported as well. I also decided on Nginx as my server of choice, so that's what I'll be describing here, but the general steps shouldn't differ too much between this and Apache.

docker-compose.yml

version: '2'
services:
################################
#         Nginx Websites       #
################################
  akpwebdesign.com:
    image: nginx:alpine
    restart: always
    expose:
      - 80
    volumes:
      - /srv/websites/nginx.conf:/etc/nginx/nginx.conf:ro
      - /srv/websites/akpwebdesign.com/:/usr/share/nginx/html/:ro

  customersite.com:
    image: nginx:alpine
    restart: always
    expose:
      - 80
    volumes:
      - /srv/websites/nginx.conf:/etc/nginx/nginx.conf:ro
      - /srv/websites/customersite.com/:/usr/share/nginx/html/:ro

################################
#            Extras            #
################################
  haproxy:
    image: haproxy:alpine
    restart: always
    ports:
      - "80:80"
    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

This docker-compose configuration sets up two nginx containers, based off of the nginx:alpine image, and one HAProxy container, based off of the haproxy:alpine image. I've chosen to use the Alpine Linux images here, because they do everything I need, but are smaller images in terms of data size.

As in our shell script before, I've specified restart: always, so that Docker will restart our containers if they crash or the host server is restarted for any reason. On each of the Nginx containers, I've exposed port 80, so that our HAProxy instance can reach them, and on our HAProxy container, I've specified that port 80 should be exposed on our host, so the outside world can reach it. I've also configured volumes for each of the containers, to provide locations outside of the containers for configuration files and content directories.

The last thing to note about the configuration is that the HAProxy container has links configured, which tell Docker to link the containers together and adds appropriate entries to the HAProxy container's hosts file.

I'm sharing the same nginx.conf file between both of the websites above, but if you'd rather have a customized configuration for each one, you can do that as well by just specifying different files.

3. Nginx configuration

nginx.conf

events {
  worker_connections  4096;  ## Default: 1024
}

http {
  include mime.types;
  gzip on;
  server_tokens off;

  set_real_ip_from 172.17.0.0/24;
  real_ip_header X-Forwarded-For;
  
  access_log /var/log/nginx/access.log;

  server {

    listen 80 default_server;
    listen [::]:80 default_server;

    root /usr/share/nginx/html;
  }
}

My nginx.conf is very plain at this stage, as I just want to get a basic setup going before I move on to anything more complex. The only thing that I'll note about it is the set_real_ip_from line. By default, Docker runs on the 172.17.0.0/24 network, so this line adds the subnet to the Nginx configuration as the 'trusted' proxy server network, allowing Nginx to properly detect that the requests it is getting are coming from another location.

4. HAProxy configuration

haproxy.cfg

global
  log /dev/log local0
  log /dev/log local1 info
  maxconn 2048

defaults
  log global
  mode http
  option httplog
  option dontlognull
  option forwardfor
  option http-server-close
  timeout connect 5000
  timeout client 50000
  timeout server 50000
  errorfile 400 /usr/local/etc/haproxy/errors/400.http
  errorfile 403 /usr/local/etc/haproxy/errors/403.http
  errorfile 408 /usr/local/etc/haproxy/errors/408.http
  errorfile 500 /usr/local/etc/haproxy/errors/500.http
  errorfile 502 /usr/local/etc/haproxy/errors/502.http
  errorfile 503 /usr/local/etc/haproxy/errors/503.http
  errorfile 504 /usr/local/etc/haproxy/errors/504.http

################
# WWW Frontend #
################
frontend www
  bind 0.0.0.0:80
  option http-server-close
  option forwardfor

  # AKP Web Design sites
  use_backend akp if { hdr(host) -i akpwebdesign.com }

  # Customer sites
  use_backend customer if { hdr(host) -i customersite.com }

  # AKP backend by default.
  default_backend akp

####################
# akpwebdesign.com #
####################
backend akp
  mode http
  server web1 akpwebdesign.com:80

####################
# customersite.com #
####################
backend customer
  mode http
  server web1 cust01:80

The HAProxy configuration is very bare bones as well at this point. I'd really only like to call out the fact that the hostnames in the backend definitions come from the link definitions in docker-compose.yml. For example, I defined a link of customersite.com:cust01, which is linking the HAProxy container to the customersite.com container, giving it an alias of cust01. If you don't provide an alias, the container name is used, as demonstrated with akpwebdesign.com.

5. Starting the stack

Once we've got our configuration files in place at the locations defined in docker-compose.yml, as well as our website content in the proper locations (also defined in docker-compose.yml), we can start up our stack! Simply run the command docker-compose up, from within the folder where you've placed your docker-compose.yml file to start the stack. *Note: The docker-compose configuration doesn't have to be in the same folder as your content and configuration files, but it helps if it's nearby.

Now, I know what you're thinking: "Now I've got all these logs on my screen, and what if I want to close this? Will it bring my websites down?"

Yes. Yes it will.

What you'll want to do, once you verify that your web servers start up properly and everything is good with them, is shut down the docker-compose stack (using Ctrl+C), then rerun the docker-compose command, specifying a new flag. docker-compose up -d tells docker-compose to run in daemon mode, which makes it run in the background. You can now disconnect from your server or close your terminal, and your Docker containers will continue running in the background. If you need to shut down or restart the stack (say, to load a new configuration), you can run docker-compose down or docker-compose restart in the directory where your docker-compose.yml is located.

6. Next steps

Right now, we've got a very basic stack running with HAProxy providing load balancing to a couple of Nginx servers. The next thing I did in improving my stack was to configure my Nginx containers for SSL, and put HAProxy in SSL passthrough mode, allowing all of my sites to have separate SSL certificates from LetsEncrypt. I'll talk about that in my next post, as well as going over how I set up a container to catch any traffic directed to LetsEncrypt, allowing me to renew my SSL certificates without taking down my websites.

Austin Peterson

Read more posts by this author.