In the previous post, I explained how to communicate with other Docker containers. If you haven’t read the post yet, go to this post first.
Communication with other Docker containers.
You can find the complete source code here
This is one of Docker learning series posts.
- Start Docker from scratch
- Docker volume
- Bind host directory to Docker container for dev-env
- Communication with other Docker containers
- Run multi Docker containers with compose file
- Container’s dependency check and health check
- Override Docker compose file to have different environments
- Creating a cluster with Docker swarm and handling secrets
- Update and rollback without downtime in swarm mode
- Container optimization
- Visualizing log info with Fluentd, Elasticsearch and Kibana
Create a docker-compose file
In order to run multiple Docker containers, we can run them one by one with necessary options but it is cumbersome to specify all options in a command line every time we want to starts them. Creating a script file to run them together is one of options but Docker offers another way called docker-compose
. Necessary configurations can be written in a file and a user can run and stop them with following commands.
# start containers
docker-compose up
# stop containers
docker-compose down
docker-compose file looks like this below. It is yaml file and you can understand what it configures at the first glance. The Docker images were built in
the previous post.
version: "3.7"
services:
log-server:
image: log-server
ports:
- "8001:80"
networks:
- app-net
restify-server:
image: restify-server
ports:
- "8002:80"
depends_on:
- log-server
networks:
- app-net
networks:
app-net:
external:
name: log-test-nat
version
: Version of docker-compose formatservices
: top level entry to specify containerslog-server
,restify-server
: Docker containers’ namesimage
: Docker container imageports
: ports which the container want to assign. If it is only"80"
for example, random port on a host is assigned.networks
: Docker network namedepends_on
: Specify the dependency. The specified Docker containers are running before this container starts.app-net
: network nameexternal
: Compose expects the network calledlog-test-nat
already exist and it doesn’t try to create it.
Run multi containers with docker-compose
My result looks like this. As you can see, the log-server
started up before restify-server
started up because restify-server
has dependency to log-server
.
$ docker-compose up
Creating docker-compose_log-server_1 ... done
Creating docker-compose_restify-server_1 ... done
Attaching to docker-compose_log-server_1, docker-compose_restify-server_1
log-server_1 | {"message":"restify listening at http://[::]:80","level":"info"}
log-server_1 | {"message":"restify-server: restify listening at http://[::]:80","level":"info"}
restify-server_1 | STATUS: 201
restify-server_1 | HEADERS: {"server":"restify","content-type":"application/json","content-length":"9","date":"Sun, 08 Nov 2020 12:17:41 GMT","connection":"close"}
restify-server_1 | BODY: "Created"
restify-server_1 | No more data in response.
The Docker containers stop when ctrl + c
is entered. You can also add an -d
option to run it in background. docker-compose
makes command simpler but it also offers additional advantage. When we want to run several containers some of them may need to be scaled up to address tons of service requests. docker-compose
offers it in very easy way. When it runs with --scale
option and the number of the target containers it starts multiple containers up. It is very easy to scale up, isn’t it? I created another docker compose file docker-compose-without-port.yml
which doesn’t specify the port bind to a host because it’s not possible to bind the same port to the multiple containers. Arbitrary port is assigned by Docker at start up in this case. The result looks like this.
$ docker-compose -f docker-compose-without-port.yml up -d --scale log-server=3
Starting docker-compose_log-server_1 ... done
Creating docker-compose_log-server_2 ... done
Creating docker-compose_log-server_3 ... done
Starting docker-compose_restify-server_1 ... done
$ docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------------------------
docker-compose_log-server_1 docker-entrypoint.sh node ... Up 0.0.0.0:32769->80/tcp
docker-compose_log-server_2 docker-entrypoint.sh node ... Up 0.0.0.0:32770->80/tcp
docker-compose_log-server_3 docker-entrypoint.sh node ... Up 0.0.0.0:32771->80/tcp
docker-compose_restify-server_1 docker-entrypoint.sh node ... Up 0.0.0.0:8002->80/tcp
The name became docker-compose_<service-name>_<number>
. docker-compose ps
lists all running containers that are part of the compose application. It shows 3 log-servers started up and different host ports were bind to them. But does it mean that restify-server
calls log API in different server depending on the performance of the containers? We expect that the load balancing functionality works because it offers scale function. But isn’t it too easy? Let’s confirm if it works!
# Send 100 http requests to restify-server
for i in {1..100}; do curl http://localhost:8002/hello/name$i > /dev/null; done
# Check if the requests are load balanced
docker container exec docker-compose_log-server_1 cat server.log
docker container exec docker-compose_log-server_2 cat server.log
docker container exec docker-compose_log-server_3 cat server.log
This is my result.
$ docker container exec docker-compose_log-server_1 cat server.log
{"message":"restify listening at http://[::]:80","level":"info"}
{"message":"restify-server: restify listening at http://[::]:80","level":"info"}
{"message":"restify listening at http://[::]:80","level":"info"}
{"message":"restify-server: GET request with param [name1]","level":"info"}
{"message":"restify-server: GET request with param [name3]","level":"info"}
{"message":"restify-server: GET request with param [name6]","level":"info"}
{"message":"restify-server: GET request with param [name8]","level":"info"}
{"message":"restify-server: GET request with param [name11]","level":"info"}
{"message":"restify-server: GET request with param [name12]","level":"info"}
{"message":"restify-server: GET request with param [name14]","level":"info"}
{"message":"restify-server: GET request with param [name18]","level":"info"}
{"message":"restify-server: GET request with param [name19]","level":"info"}
{"message":"restify-server: GET request with param [name20]","level":"info"}
{"message":"restify-server: GET request with param [name23]","level":"info"}
{"message":"restify-server: GET request with param [name24]","level":"info"}
{"message":"restify-server: GET request with param [name26]","level":"info"}
{"message":"restify-server: GET request with param [name32]","level":"info"}
{"message":"restify-server: GET request with param [name35]","level":"info"}
{"message":"restify-server: GET request with param [name36]","level":"info"}
{"message":"restify-server: GET request with param [name42]","level":"info"}
{"message":"restify-server: GET request with param [name44]","level":"info"}
{"message":"restify-server: GET request with param [name47]","level":"info"}
{"message":"restify-server: GET request with param [name49]","level":"info"}
{"message":"restify-server: GET request with param [name50]","level":"info"}
{"message":"restify-server: GET request with param [name58]","level":"info"}
{"message":"restify-server: GET request with param [name59]","level":"info"}
{"message":"restify-server: GET request with param [name60]","level":"info"}
{"message":"restify-server: GET request with param [name62]","level":"info"}
{"message":"restify-server: GET request with param [name65]","level":"info"}
{"message":"restify-server: GET request with param [name66]","level":"info"}
{"message":"restify-server: GET request with param [name67]","level":"info"}
{"message":"restify-server: GET request with param [name69]","level":"info"}
{"message":"restify-server: GET request with param [name76]","level":"info"}
{"message":"restify-server: GET request with param [name81]","level":"info"}
{"message":"restify-server: GET request with param [name84]","level":"info"}
{"message":"restify-server: GET request with param [name85]","level":"info"}
{"message":"restify-server: GET request with param [name87]","level":"info"}
{"message":"restify-server: GET request with param [name89]","level":"info"}
{"message":"restify-server: GET request with param [name95]","level":"info"}
{"message":"restify-server: GET request with param [name97]","level":"info"}
{"message":"restify-server: GET request with param [name98]","level":"info"}
$ docker container exec docker-compose_log-server_2 cat server.log
{"message":"restify listening at http://[::]:80","level":"info"}
{"message":"restify-server: GET request with param [name5]","level":"info"}
{"message":"restify-server: GET request with param [name7]","level":"info"}
{"message":"restify-server: GET request with param [name9]","level":"info"}
{"message":"restify-server: GET request with param [name13]","level":"info"}
{"message":"restify-server: GET request with param [name16]","level":"info"}
{"message":"restify-server: GET request with param [name17]","level":"info"}
{"message":"restify-server: GET request with param [name22]","level":"info"}
{"message":"restify-server: GET request with param [name37]","level":"info"}
{"message":"restify-server: GET request with param [name38]","level":"info"}
{"message":"restify-server: GET request with param [name39]","level":"info"}
{"message":"restify-server: GET request with param [name40]","level":"info"}
{"message":"restify-server: GET request with param [name43]","level":"info"}
{"message":"restify-server: GET request with param [name51]","level":"info"}
{"message":"restify-server: GET request with param [name54]","level":"info"}
{"message":"restify-server: GET request with param [name57]","level":"info"}
{"message":"restify-server: GET request with param [name61]","level":"info"}
{"message":"restify-server: GET request with param [name63]","level":"info"}
{"message":"restify-server: GET request with param [name64]","level":"info"}
{"message":"restify-server: GET request with param [name68]","level":"info"}
{"message":"restify-server: GET request with param [name70]","level":"info"}
{"message":"restify-server: GET request with param [name71]","level":"info"}
{"message":"restify-server: GET request with param [name74]","level":"info"}
{"message":"restify-server: GET request with param [name78]","level":"info"}
{"message":"restify-server: GET request with param [name79]","level":"info"}
{"message":"restify-server: GET request with param [name80]","level":"info"}
{"message":"restify-server: GET request with param [name82]","level":"info"}
{"message":"restify-server: GET request with param [name83]","level":"info"}
{"message":"restify-server: GET request with param [name90]","level":"info"}
{"message":"restify-server: GET request with param [name91]","level":"info"}
{"message":"restify-server: GET request with param [name96]","level":"info"}
{"message":"restify-server: GET request with param [name100]","level":"info"}
$ docker container exec docker-compose_log-server_3 cat server.log
{"message":"restify listening at http://[::]:80","level":"info"}
{"message":"restify-server: restify listening at http://[::]:80","level":"info"}
{"message":"restify-server: GET request with param [name2]","level":"info"}
{"message":"restify-server: GET request with param [name4]","level":"info"}
{"message":"restify-server: GET request with param [name10]","level":"info"}
{"message":"restify-server: GET request with param [name15]","level":"info"}
{"message":"restify-server: GET request with param [name21]","level":"info"}
{"message":"restify-server: GET request with param [name25]","level":"info"}
{"message":"restify-server: GET request with param [name27]","level":"info"}
{"message":"restify-server: GET request with param [name28]","level":"info"}
{"message":"restify-server: GET request with param [name29]","level":"info"}
{"message":"restify-server: GET request with param [name30]","level":"info"}
{"message":"restify-server: GET request with param [name31]","level":"info"}
{"message":"restify-server: GET request with param [name33]","level":"info"}
{"message":"restify-server: GET request with param [name34]","level":"info"}
{"message":"restify-server: GET request with param [name41]","level":"info"}
{"message":"restify-server: GET request with param [name45]","level":"info"}
{"message":"restify-server: GET request with param [name46]","level":"info"}
{"message":"restify-server: GET request with param [name48]","level":"info"}
{"message":"restify-server: GET request with param [name52]","level":"info"}
{"message":"restify-server: GET request with param [name53]","level":"info"}
{"message":"restify-server: GET request with param [name55]","level":"info"}
{"message":"restify-server: GET request with param [name56]","level":"info"}
{"message":"restify-server: GET request with param [name72]","level":"info"}
{"message":"restify-server: GET request with param [name73]","level":"info"}
{"message":"restify-server: GET request with param [name75]","level":"info"}
{"message":"restify-server: GET request with param [name77]","level":"info"}
{"message":"restify-server: GET request with param [name86]","level":"info"}
{"message":"restify-server: GET request with param [name88]","level":"info"}
{"message":"restify-server: GET request with param [name92]","level":"info"}
{"message":"restify-server: GET request with param [name93]","level":"info"}
{"message":"restify-server: GET request with param [name94]","level":"info"}
{"message":"restify-server: GET request with param [name99]","level":"info"}
It worked as expected. It looks that each container handled similar number of requests. How easy it is to scale up!
Use specific port range
Maybe we want to use the specific port range. It is defined in docker-compose-with-port-range.yml
in the following way.
ports:
- "8003-8010:80"
Just specify the range on left side. In this example, port 8003 – 8010 will be used and max number of containers is 8. Of course, some containers don’t start up when specifying bigger number. In following result, log-server 5 and 10 were failed to start because port number couldn’t be assigned.
$ docker-compose -f docker-compose-with-port-range.yml up -d --scale log-server=10
The Docker Engine you're using is running in swarm mode.
Compose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node.
To deploy your application across the swarm, use `docker stack deploy`.
The "log-server" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.
Creating docker-compose_log-server_1 ... done
Creating docker-compose_log-server_2 ... done
Creating docker-compose_log-server_3 ... done
Creating docker-compose_log-server_4 ... done
Creating docker-compose_log-server_5 ... error
Creating docker-compose_log-server_6 ... done
Creating docker-compose_log-server_7 ... done
Creating docker-compose_log-server_8 ... done
Creating docker-compose_log-server_9 ... error
Creating docker-compose_log-server_10 ... done
ERROR: for docker-compose_log-server_9 Cannot start service log-server: Ports are not available: listen tcp 0.0.0.0:8010: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
ERROR: for docker-compose_log-server_5 Cannot start service log-server: Ports are not available: listen tcp 0.0.0.0:8008: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
ERROR: for log-server Cannot start service log-server: Ports are not available: listen tcp 0.0.0.0:8010: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
Encountered errors while bringing up the project.
Conclusion
We could easily configure all Docker containers that we want them to interact with each other. Download my code sample and try to play with it if you haven’t tried it on your PC yet!
Comments