docker-compose modularization
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.
docker-compose.ymlversion: '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: localTo 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 masterThis 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
docker-compose.ymlversion: '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: localSo, 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?
docker-compose-agent.ymlversion: '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}:/workspaceIn 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 -dWe 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.
.envCOMPOSE_FILE=docker-compose.yml:docker-compose-agent.ymlNow we can just do docker-compose up -d.
