Docker compose

Compose is a tool for defining and running multi-container Docker applications. You can create a single docker compose configuration yml file that defines list of your container services.

Followings are some of the benefits of using docker compose:

  • Compose works in all environments: production, staging, development, testing, as well as CI workflows.
  • With a single command, you create and start all the services or containers
  • You can define common networks and volumes for your containers

Consider that you are working on a bigger project where you are dealing with multiple containers. Using command line it would be very difficult to manage multiple containers rather using docker compose you need to remember few commands to manage all your services.

How does docker compose work?

Using Compose is basically a three-step process:

  • Define your app’s environment with a Dockerfile
  • Define the services that make up your app in docker-compose.yml so they can be run together in an isolated environment.
  • Run docker-compose up and Compose starts and runs your entire app.

Followings are some of the commands that we will use for our tutorial:

Creating a project using docker-compose

Let's learn how we can create an application using docker compose. Imagine we are building a website where we show number of visits each time a user visits our website for our example we will be using following two services:

  • NodeJs
  • Redis

Let's understand this by diagram:

To create multi-container service we will follow the steps below:

  • We will create a custom docker image for our node server
  • We will use existing redis docker image from docker hub
  • We will define these two services in our docker-compose.yml file
  • We will use docker compose commands to create or destroy our containers

Let's create a new folder and start adding package.json file first.

{
  "dependencies": {
    "express": "*",
    "redis": "2.8.0"
  },
  "scripts": {
    "start": "node index.js"
  }
}

In our package.json file we have added two dependencies that we require for our project. Now, let's create a index.js  file that define a route and a redis logic to increment our visits.

const redis = require('redis');
const express = require('express');

const app = express();
const client = redis.createClient({
  port: 6379,
  host: 'redis-server'
});

// Set default visits
client.set('visits', 0);

// increment visits on each
// visits of the user
app.get('/', (req, res) => {
  client.get('visits', (err, visits) => {
    res.send('Total Visits: ' + visits);
    client.set('visits', parseInt(visits) + 1);
  });
});

// listen on 3000
app.listen(3000, () => {
  console.log('Listening on port 3000');
});

Now, we will define Dockerfile  to create our custom nodejs docker image that we will later on use in our docker-compose.yml file.

Open your terminal window and create a new file called Dockerfile with following contents:

# get the base image
# from docker hub
FROM node:alpine

# define a working directory
# for our container to use
WORKDIR /app

# copy package.json file to our working dir
COPY package.json /app

# install our dependencies
RUN npm install

# copy other files
COPY . .

# run a startup command
# to run our application
CMD ["npm", "start"]

So far we have following files in our project:

  • package.json
  • Dockerfile
  • index.js

Finally, we will create a docker-compose.yml file which defines our containers. Create docker-compose.yml file with following contents:

version: '3'
services:
  redis-server:
    image: redis
    container_name: redis
  node-server:
    build: .
    container_name: node
    restart: always
    ports:
      - '8080:3000'

Let's understand our docker compose file syntax:

Variable Defination
version version of docker-compose that we are using
services List of containers that we are going to create
image base image that we use from docker hib
container_name a user friendly name of our container
build location of Dockerfile
restart what should we do in case our container fails?
ports mapping container port to our host port       i.e.  HOST:CONTAINER

So far we have all necessary files that we need in order to create containers using docker compose. Make sure you have docker installed on your machine before you are able to run following commands:

# create and start our containers
# keep in mind -d option runs container in background
docker-compose up -d

# list of all our running containers
docker ps

# check process logs for our node app
docker logs -f node

Once you run all of the above commands you will have a running application on your local machine at http://localhost:8080/

Let's review our index.js file and find following line of code:

const client = redis.createClient({
  port: 6379,
  host: 'redis-server'
});

Please note that we did not use 127.0.0.1 as our redis host however we used redis-server  string. The reason why we did this because:

  • Our containers are isolated
  • They do not know about each other
  • Docker compose creates a default network for both containers
  • This network allow both container to access each other using their service name as the host name
  • Find our service name for our redis container . i.e. redis-server

Consider following diagram to clearly understand between your computer (host) and docker server (docker client).

Following are some important considerations while working with docker:

  • Your host machine or localhost does not have direct access to containers
  • There are two ports a local port for container and a host port
  • To access specific port within a docker container you need to define ports block in docker-compose file.

I hope I was able to explain about how to create multi-container app using docker compose. To destroy your running containers run following command:

docker-compose down

Source Code

Source code for this tutorial can be found on github. Click the link below to download the source code.

Docker Compose Example