In this example we are going to create two identical Apache servers and one HAProxy container. When we want to access our website, we will be calling HAProxy, not the Apache servers. HAProxy will divert traffic to Apache servers in "round-robin" fashion.


Flow


  1. Request goes to HAProxy container.

  2. HAProxy container calls either Apache container 1 or 2.

  3. Response is served by Apache container 1 or 2.

Structure


ubuntu@linux:~/helloworld$ tree -a
.
└── docker
├── apache
│ ├── 1
│ │ ├── Dockerfile
│ │ └── index.html
│ └── 2
│ ├── Dockerfile
│ └── index.html
├── docker-compose.yml
├── .env
└── haproxy
├── Dockerfile
└── haproxy.cfg

5 directories, 8 files

Files


docker/apache/1/Dockerfile


ubuntu@linux:~/helloworld$ cat docker/apache/1/Dockerfile 
FROM httpd:2.4

COPY index.html /usr/local/apache2/htdocs/index.html

docker/apache/1/index.html


ubuntu@linux:~/helloworld$ cat docker/apache/1/index.html 
Serving from Apache Server 1

docker/apache/2/Dockerfile


ubuntu@linux:~/helloworld$ cat docker/apache/2/Dockerfile 
FROM httpd:2.4

COPY index.html /usr/local/apache2/htdocs/index.html

docker/apache/2/index.html


ubuntu@linux:~/helloworld$ cat docker/apache/2/index.html 
Serving from Apache Server 2

docker/haproxy/Dockerfile


ubuntu@linux:~/helloworld$ cat docker/haproxy/Dockerfile 
FROM haproxy:1.7

COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

docker/haproxy/haproxy.cfg


You can access to HAProxy GUI via http://host_os_ip/haproxy?stats and login with admin:admin.


ubuntu@linux:~/helloworld$ cat docker/haproxy/haproxy.cfg 
global
log /dev/log local0
log localhost local1 notice
maxconn 2000
daemon

defaults
log global
mode http
option httplog
option dontlognull
retries 3
timeout connect 5000
timeout client 50000
timeout server 50000

frontend http-in
bind *:80
default_backend webservers

backend webservers
stats enable
stats auth admin:admin
stats uri /haproxy?stats
balance roundrobin
option httpchk
option forwardfor
option http-server-close
server apache1 ${APACHE_1_IP}:${APACHE_EXPOSED_PORT} check
server apache2 ${APACHE_2_IP}:${APACHE_EXPOSED_PORT} check

docker/.env


ubuntu@linux:~/helloworld$ cat docker/.env 
COMPOSE_PROJECT_NAME=helloworld

APACHE_EXPOSED_PORT=80

APACHE_1_IP=192.168.0.11
APACHE_2_IP=192.168.0.22

HA_PROXY_IP=192.168.0.33

NETWORK_SUBNET=192.168.0.0/24

docker/docker-compose.yml


ubuntu@linux:~/helloworld$ cat docker/docker-compose.yml 
version: '3'

services:
apache_img_1:
container_name: ${COMPOSE_PROJECT_NAME}_apache_con_1
build: ./apache/1
expose:
- ${APACHE_EXPOSED_PORT}
networks:
public_net:
ipv4_address: ${APACHE_1_IP}
apache_img_2:
container_name: ${COMPOSE_PROJECT_NAME}_apache_con_2
build: ./apache/2
expose:
- ${APACHE_EXPOSED_PORT}
networks:
public_net:
ipv4_address: ${APACHE_2_IP}
haproxy_img:
build: ./haproxy
ports:
- 80:80
expose:
- 80
networks:
public_net:
ipv4_address: ${HA_PROXY_IP}
environment:
- APACHE_1_IP=${APACHE_1_IP}
- APACHE_2_IP=${APACHE_2_IP}
- APACHE_EXPOSED_PORT=${APACHE_EXPOSED_PORT}
networks:
public_net:
driver: bridge
ipam:
driver: default
config:
- subnet: ${NETWORK_SUBNET}

Validation


Validate "docker-compose.yml" file and see the mapping.


ubuntu@linux:~/helloworld/docker$ docker-compose config
networks:
public_net:
driver: bridge
ipam:
config:
- subnet: 192.168.0.0/24
driver: default
services:
apache_img_1:
build:
context: /home/ubuntu/helloworld/docker/apache/1
container_name: helloworld_apache_con_1
expose:
- '80'
networks:
public_net:
ipv4_address: 192.168.0.11
apache_img_2:
build:
context: /home/ubuntu/helloworld/docker/apache/2
container_name: helloworld_apache_con_2
expose:
- '80'
networks:
public_net:
ipv4_address: 192.168.0.22
haproxy_img:
build:
context: /home/ubuntu/helloworld/docker/haproxy
environment:
APACHE_1_IP: 192.168.0.11
APACHE_2_IP: 192.168.0.22
APACHE_EXPOSED_PORT: '80'
expose:
- 80
networks:
public_net:
ipv4_address: 192.168.0.33
ports:
- 80:80/tcp
version: '3.0'

Build


When you run command below without -d flag, you will see HAProxy pinging Apache servers every 1 seconds. This proves that our setup works fine.


ubuntu@linux:~/helloworld/docker$ docker-compose up

Creating network "helloworld_public_net" with driver "bridge"
Building haproxy_img
Successfully tagged helloworld_haproxy_img:latest

Building apache_img_2
Successfully tagged helloworld_apache_img_2:latest

Building apache_img_1
Successfully tagged helloworld_apache_img_1:latest

Creating helloworld_apache_con_1 ... done
Creating helloworld_apache_con_1 ...
Creating helloworld_apache_con_2 ...

helloworld_apache_con_1 | 192.168.0.33 - - [03/Feb/2018:21:15:29 +0000] "OPTIONS / HTTP/1.0" 200 -
helloworld_apache_con_2 | 192.168.0.33 - - [03/Feb/2018:21:15:29 +0000] "OPTIONS / HTTP/1.0" 200 -
helloworld_apache_con_1 | 192.168.0.33 - - [03/Feb/2018:21:15:31 +0000] "OPTIONS / HTTP/1.0" 200 -
helloworld_apache_con_2 | 192.168.0.33 - - [03/Feb/2018:21:15:33 +0000] "OPTIONS / HTTP/1.0" 200 -
helloworld_apache_con_1 | 192.168.0.33 - - [03/Feb/2018:21:15:33 +0000] "OPTIONS / HTTP/1.0" 200 -
helloworld_apache_con_2 | 192.168.0.33 - - [03/Feb/2018:21:15:31 +0000] "OPTIONS / HTTP/1.0" 200 -
helloworld_apache_con_1 | 192.168.0.33 - - [03/Feb/2018:21:15:35 +0000] "OPTIONS / HTTP/1.0" 200 -
...

You must configure Apache not to log pinging request coming from HAProxy otherwise Apache logs will be bloated quickly. If you want to see how it is done, head "HAProxy" section in my blog and apply what it says under "Webserver Logs" header.


Confirmation


If you want to see the details of each element, you can run docker inspect command.


Images


ubuntu@linux:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld_apache_img_2 latest 6f63653a9e68 9 minutes ago 177MB
helloworld_haproxy_img latest 363551ccafe6 9 minutes ago 136MB
helloworld_apache_img_1 latest 83bc617be089 9 minutes ago 177MB
...

Network


ubuntu@linux:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c72c538d9025 helloworld_public_net bridge local
...

Containers


ubuntu@linux:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4e4291f3a95c helloworld_apache_img_2 "httpd-foreground" 11 minutes ago Up 11 minutes 80/tcp helloworld_apache_con_2
ebba54230552 helloworld_apache_img_1 "httpd-foreground" 11 minutes ago Up 11 minutes 80/tcp helloworld_apache_con_1
a770b68939c5 helloworld_haproxy_img "/docker-entrypoin..." 11 minutes ago Up 11 minutes 0.0.0.0:80->80/tcp helloworld_haproxy_img_1

Test


You can do benchmark with command below. It will send total of 10000 requests and 30 concurrent requests at a time. When you run this command, you can also run htop command in one of the Apache containers to see how CPU and Memory are used.


ubuntu@linux:~$ ab -n 10000 -c 30 http://192.168.0.33/

This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.0.33 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests

Server Software: Apache/2.4.29
Server Hostname: 192.168.0.33
Server Port: 80

Document Path: /
Document Length: 29 bytes

Concurrency Level: 30
Time taken for tests: 2.943 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 2730000 bytes
HTML transferred: 290000 bytes
Requests per second: 3397.93 [#/sec] (mean)
Time per request: 8.829 [ms] (mean)
Time per request: 0.294 [ms] (mean, across all concurrent requests)
Transfer rate: 905.89 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 6
Processing: 0 9 5.4 8 153
Waiting: 0 8 5.4 8 153
Total: 0 9 5.4 8 154

Percentage of the requests served within a certain time (ms)
50% 8
66% 9
75% 9
80% 10
90% 11
95% 13
98% 16
99% 25
100% 154 (longest request)

While both Apache servers are running.


ubuntu@linux:~$ for i in {1..10}; do curl 192.168.0.33:80; done

Serving from Apache Server 1
Serving from Apache Server 2
Serving from Apache Server 1
Serving from Apache Server 2
...

While only Apache 1 server is running.


ubuntu@linux:~$ for i in {1..10}; do curl 192.168.0.33:80; done

Serving from Apache Server 1
Serving from Apache Server 1
Serving from Apache Server 1
Serving from Apache Server 1
...

While only Apache 2 server is running.


ubuntu@linux:~$ for i in {1..10}; do curl 192.168.0.33:80; done

Serving from Apache Server 2
Serving from Apache Server 2
Serving from Apache Server 2
Serving from Apache Server 2
...

Apache servers are down.


ubuntu@linux:~$ curl 192.168.0.33:80

<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>