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.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
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?
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.
.env
COMPOSE_FILE=docker-compose.yml:docker-compose-agent.yml
Now we can just do docker-compose up -d
.