Multi containers apps without docker-compose
by Denis Kovalev
Disclaimer
Best way to run multi container application is to use docker-compose. This article is purely for education purposes and for those that want to understand how multiple docker containers can be tied together and work as a single application.
Multi container apps
Before we start, I’d recommend reading official Docker instructions on multi container apps because we will use the same example setup.
Example there is about running MySQL database together with the sample applications in one network. Here’s a short recap of what the official documentation is about.
- Create the network
docker network create todo-app
- Start a MySQL container and attach it to the network
docker run -d \ --network todo-app --network-alias mysql \ -v todo-mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=todos \ mysql:5.7
- Start the app and connect it to the network and database:
docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ --network todo-app \ -e MYSQL_HOST=mysql \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=secret \ -e MYSQL_DB=todos \ node:12-alpine \ sh -c "yarn install && yarn run dev"
The drawback of this approach is that it does not do automatic cleanup: database container is not stopped automatically and the network is not removed after the app terminates.
Run containers together as a single application
Our goal is to have a single command (spoiler alert: it will be a shell script) to run everything together and clean up all the resources automatically. We will use trap
command for cleanup. To stop containers we’ll need their IDs; handy that docker run
command returns the ID of the created container that we’ll save into a variable.
Let’s create a simple script combining everything. Note that the original example required local Node application so I replaced the run command with long sleep
to emulate the server continuous run:
#!/bin/bash -
echo -n "Creating network..."
docker network create todo-app >/dev/null
echo "OK"
echo -n "Starting DB..."
db=$(docker run --rm --detach \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:5.7 2>/dev/null)
echo "OK"
echo -n "Starting the app..."
app=$(docker run --rm --detach -p 3000:3000 -w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:12-alpine sh -c "sleep 3600" 2>/dev/null)
# emulate "eternal" application run
echo "OK"
trap """echo -en '\nStopping the application...';
docker stop $app >/dev/null;
echo -en 'OK\nStopping the DB...';
docker stop $db >/dev/null;
echo -en 'OK\nStopping the network...';
docker network rm todo-app >/dev/null;
echo 'OK';
""" TERM KILL EXIT
echo "Application is running. Use Ctrl-C to terminate."
# As the app container runs "forever" in detached mode,
# we should keep this script also running,
# otherwise all containers will be terminated upon EXIT.
while :; do sleep 1; done
It may look rather long, but you can remove all echo
commands that I added for nice output.
By default its run will look like this:
./app.sh
Creating network...OK
Starting DB...OK
Starting the app...OK
Application is running. Use Ctrl-C to terminate.
^C
Stopping the application...OK
Stopping the DB...OK
Stopping the network...OK
Using default network
If you want to reduce the script size even futher, you may drop network creation, that means that default bridge network will be used. Note that it’s not recommended if you are going to use this approach on remote server that runs multiple other containers and their names may collide. So the smallest script is like this:
#!/bin/bash -
db=$(docker run --rm --detach \
--network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:5.7 2>/dev/null)
app=$(docker run --rm --detach -p 3000:3000 -w /app -v "$(pwd):/app" \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:12-alpine sh -c "sleep 3600" 2>/dev/null)
trap "docker stop $app; docker stop $db;" TERM KILL EXIT
while :; do sleep 1; done
Run tests
You can use this approach to run unittest (or any other command that terminates automatically) for your application. In this case you don’t need to run app
container in detached mode and use eternal sleep
in your script. Given your app uses make unittests
command to run tests:
#!/bin/bash -
db=$(docker run --rm --detach \
--network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:5.7 2>/dev/null)
trap "docker stop $db;" EXIT
docker run --rm -w /app -v "$(pwd):/app" \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:12-alpine sh -c "make unittests"
Subscribe via RSS