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.

  1. Create the network
    docker network create todo-app
    
  2. 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
    
  3. 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"