Docker is a pretty neat technology for packaging standalone apps so people can use them without worrying a whole lot about having the same distribution and library versions as the packager.
Docker itself is concerned about just one container at a time. To get a set of related containers working together, you can use *docker-compose* to orchestrate a number of related Docker containers as a group, and start/stop then with simple ‘docker-compose up’ and ‘docker-compose down’ commands. For this article, I’m assuming you know how to create, start and stop a single Docker container. If you don’t, go learn that first. Go ahead. I’ll wait!
The code for this article is on github.com at https://github.com/timbaileyjones/docker-compose-loadbalancer-example
The services this sample creates and orchestrates are:
- two instances of an example webapp in python/flask (container name is flask-site)
- two instances of a different example app in node/express (container name is node-site, original, right?)
- nginx - a load balancer/reverse proxy that will host all four of the above containers within a host
Additionally, this example shows how to create a shared volume to collect logs for all containers. The final touch is that the instances of the flask-site cannot reach the node-site, and vice-versa.
Let’s take a walk through the docker-compose.yml file.
Overall service name definitions
This is where these relationships are defined.
The above lines define the names of the services (which I made up).
- flask-site-a, flask-site-b
- node-site-a, node-site-b
The ‘build:’ property following each service name is merely the name of the docker container name that the service will be built from. In turn, each of those are subdirectories of this project, and each of those has a Dockerfile that builds that service.
Note that flask-site-a and flask-site-b build from the SAME container: flask-site. Similarly, the node-site-a and node-site-b build from the SAME container: node-site. This is how I get the two instance of each service.
For some reason, I wasn’t able to get a docker-compose script that knew about version:’3’ to work. Version 3 of the docker-compse file format is supposed to support a ‘scale:’ property where I probably could have done the same thing with a ‘scale: 2’ line. I need to circle back to this and figure it out. But for now, I’ll use the -a and -b suffixes to get my two instances.
These service names become entries in each container’s /etc/hosts files, prepopulated with whatever IP address Docker comes up with. So, you can refer to the other containers by their services names, without worrying about where they are on the network.
By default, docker-compose will co-locate every service on the same docker network, which is nice and easily. Here I will demonstrate how to set up multiple ISOLATED networks. Turns out to be really simple: All you have to do is give each isolated network a name, and give it a ‘driver: bridge’ line.
You can put section at the top of the docker-compose.yml before the service names, but I put it at the very bottom of the file, because I want the reader to focus on the overall set of managed service names.
Then, with you specify those names under each services’ ‘networks:’
Let’s focus on the load-balancer section next:
The ‘volumes:’ property makes files and directories on the host appear inside the containers.
- the nginx.conf file will appear as /etc/nginx/nginx.conf (which is where nginx expect to find it).
- /tmp/shared_logs is a directory, and will be available at /tmp/shared_logs inside nginx also.
By default, each container can only access the external network, and it’s own Docker network, but not any other Docker networks. The ‘links:’ property allows you to specify exceptions to this isolation. So here, with these four lines, we are explicitly allowing the load-balancer container to reach the four example services (2 for flask, and 2 for node)
The reason, of course, is that the load balancer configuration creates connections to these other containers in response to incoming connections. (See next code block for nginx.conf)
The ‘networks:’ property allows the preceding ‘links:’ values to work.
flask-site / node-site details
Continuing with the docker-compose.yml file, we define two instances of flask-site and node-site. Notice that the flask-site-a and flask-site-b are declared to be in the ‘flask-network’ (defined above at line 70). Similarly, the node-site-a and node-site-b containers are on the flask-network network. This means that the flask containers cannot connect to the node containers and vice-versa.
Also, notice the ports: mapping have to differ on the host side (8081, 8082, 3001, 3002).
Inside the flask-site container
flask-site/Dockerfile is a super-simple container,
including basic python/flask app on top of the latest
It writes its log files with /tmp/shared_logs, which is visible on the host.
And it exposes just one port number.
I included the iputils-ping package in the apt-get command
so that I would be able to do
a ‘docker exec -it
Covering the flask library is outside of the scope of this article.
Inside the node-site container
node-site/Dockerfile is a super-simple container, built on top of the latest node container. Just like flask-container, it writes its log files to /tmp/shared_logs. I included the iputils-ping package here too, for the same reason.
Covering the nodejs platform and express library has been covered fabulously by others on the web. I could have, of course, chosen to package any kind of webapp here (java/tomcat, java/jetty, python/django, etc)
Bring it up
Bring it down