06 February 2017, Martin Ahrer

In this blog post we are looking into how we can create modular compose projects.

With docker-compose we can describe a bunch of containers and container related resources such as networks and volumes that make up an application. All this is usually going into a docker-compose.yml file.

As an application grows complex its worth to consider modularizing compose descriptors. Instead of stuffing each and every item into docker-compose.yml we can split out individual containers. This gives us the flexibility to build optional containers that we just load in certain environments. Or we just do it to manage complexity just like we do it with ordinary source code.

Let’s look at some real scenario. We have to run a Jenkins build server made up from a master and an agent. Below we find a typical compose descriptor which can get really big as the number of containers it is describing is growing.

Monolithic all-in-one docker-compose.yml
version: '2'
services:
  master:
    image: softwarecraftsmen/jenkins-master:${TAG}
    restart: always
    environment:
      - JAVA_OPTS = "-Djava.awt.headless=true"
      - JENKINS_URL
      - JENKINS_ADMIN_USERNAME
      - JENKINS_ADMIN_PASSWORD
    ports:
      - "${JENKINS_AGENT_PORT}:50000"
      - "${JENKINS_HTTP_PORT}:8080"
    volumes:
      - home:/var/jenkins_home/

  agent:
    image: softwarecraftsmen/jenkins-swarm-agent:0.1
    restart: always
    hostname: agent
    environment:
      - COMMAND_OPTIONS=-master http://master:8080 -username ${JENKINS_ADMIN_USERNAME} -password ${JENKINS_ADMIN_PASSWORD} -labels 'docker' -executors ${JENKINS_AGENT_EXECUTORS}
      - JENKINS_AGENT_WORKSPACE
    depends_on:
      - master
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${JENKINS_AGENT_WORKSPACE}:/workspace

volumes:
  home:
    driver: local

To bring up the entire system we would just do docker-compose up -d. Convenient isn’t it?

Now we may not always require an agent, for demoing purpose we may just want to work with a master. At first we are tempted to just use docker-compose up master to bring up the master container. But look, we still have to deal with configuration (variable JENKINS_AGENT_PORT) bound to the master but related to agents only. Knowing that we don’t need the agent stuff still we would run the following

export JENKINS_AGENT_PORT=50000
docker-compose up master

This is kind of destroying a nice docker-compose experience. So let’s look how we can do better by applying good practices known from software-engineering.

Modularization is your friend

Master docker-compose.yml
version: '2'

services:
  master:
    image: softwarecraftsmen/jenkins-master:${TAG}
    restart: always
    environment:
      - JAVA_OPTS = "-Djava.awt.headless=true"
      - JENKINS_URL
      - JENKINS_ADMIN_USERNAME
      - JENKINS_ADMIN_PASSWORD
    ports:
      - "${JENKINS_HTTP_PORT}:8080"
    volumes:
      - home:/var/jenkins_home/

volumes:
  home:
    driver: local

So, for running only a master we would run docker-compose up -d. Now let’s look at how we refactored the agent into its own unit. We stripped off all agent related configuration and put it into docker-compose-agent.yml. Do you notice that we even extracted some of the master container configuration?

Agent docker-compose-agent.yml
version: '2'
services:
  master:
    ports:
      - "${JENKINS_AGENT_PORT}:50000"

  agent:
    image: softwarecraftsmen/jenkins-swarm-agent:0.1
    restart: always
    hostname: agent
    environment:
      - COMMAND_OPTIONS=-master http://master:8080 -username ${JENKINS_ADMIN_USERNAME} -password ${JENKINS_ADMIN_PASSWORD} -labels 'docker' -executors ${JENKINS_AGENT_EXECUTORS}
      - JENKINS_AGENT_WORKSPACE
    depends_on:
      - master
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${JENKINS_AGENT_WORKSPACE}:/workspace

In case we decide to add an agent we just have to load all required compose descriptors like docker-compose -f docker-compose.yml -f docker-compose-agent.yml up -d.

To see the effective descriptor we can use docker-compose -f docker-compose.yml -f docker-compose-agent.yml config.

We have well designed components now which we can use individually or merge into a complex application.

But wait this is not yet the end of the story. Having to remember all of the compose descriptors is a bit tedious. Let’s improve on this a bit further.

docker-compose honors the environment variable COMPOSE_FILE. Using this we can run

export COMPOSE_FILE=docker-compose.yml:docker-compose-agent.yml (1)
docker-compose up -d

We can stretch a bit further I think, this is not yet where we want to be. docker-compose also loads environment variables from a .env file located at the current directory. By putting docker-compose variables there we can make the compose command even simpler.

File .env
COMPOSE_FILE=docker-compose.yml:docker-compose-agent.yml

Now we can just do docker-compose up -d.

Docker
docker-compose