Monthly Archives: July 2015

Building docker containers with make on CoreOS

In this post, I’ll show how you can use a Makefile to build docker containers, and how to orchestrate the builds of containers which have build-time dependencies. Finally, I’ll show you an easy way to install make into CoreOS.

When I began my docker journey, I started out with shell scripts which would prepare the build context before running docker build, then performing cleanups.

Any non-trivial dockerized application will be made up of many containers, and it wasn’t long before these scripts started to get more elaborate, and I felt that using a Makefile would help me accelerate build times by eliminating unchanged steps in the process.

Not only that, a Makefile communicates the intent and the dependencies better to other developers.

I’m not an expert on make, but I’ve picked up a few useful tricks and I’ve boiled them down into this sample (note, Makefiles are finicky about tabs, so I’ve put this up on a gist)

#-----------------------------------------------------------------------------
# configuration - see also 'make help' for list of targets
#-----------------------------------------------------------------------------

# name of container 
CONTAINER_NAME = myregistry.example.com:5000/mycontainer:latest

# list of dependencies in the build context - this example just finds all files
# in the 'files' subfolder 
DEPS = $(shell find myfiles -type f -print)

# name of instance and other options you want to pass to docker run for testing
INSTANCE_NAME = mycontainer
RUN_OPTS =

#-----------------------------------------------------------------------------
# default target
#-----------------------------------------------------------------------------

all   : ## Build the container - this is the default action
all: build

#-----------------------------------------------------------------------------
# build container
#-----------------------------------------------------------------------------

.built: . $(DEPS)
	docker build -t $(CONTAINER_NAME) .
	@docker inspect -f '{{.Id}}' $(CONTAINER_NAME) > .built

build : ## build the container
build: .built
 
clean : ## delete the image from docker
clean: stop
	@$(RM) .built
	-docker rmi $(CONTAINER_NAME)

re    : ## clean and rebuild
re: clean all

#-----------------------------------------------------------------------------
# repository control
#-----------------------------------------------------------------------------

push  : ## Push container to remote repository
push: build
	docker push $(CONTAINER_NAME)

pull  : ## Pull container from remote repository - might speed up rebuilds
pull:
	docker pull $(CONTAINER_NAME)

#-----------------------------------------------------------------------------
# test container
#-----------------------------------------------------------------------------

run   : ## Run the container as a daemon locally for testing
run: build stop
	docker run -d --name=$(INSTANCE_NAME) $(RUN_OPTS) $(CONTAINER_NAME)

stop  : ## Stop local test started by run
stop:
	-docker stop $(INSTANCE_NAME)
	-docker rm $(INSTANCE_NAME)

#-----------------------------------------------------------------------------
# supporting targets
#-----------------------------------------------------------------------------

help  : ## Show this help.
	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'

.PHONY : all build clean re push pull run stop help

Once this is configured and sitting alongside your Dockerfile, all you need to to is run make and it will build your container. If you’re feeling confident, you can `make push` to build and push in one step.

The default build target depends on a file called .built. You can see that .built depends on the current directory, plus any files defined in the DEPS variable. If any of those dependencies are newer than the build, then docker build will be executed, and the .built file is updated with a new timestamp.

This file also has a push target, which ensures the build is up to date, and pushes to your remote registry.

There’s also some simple targets for run and stop so you can quickly test a container after building.

Finally, there’s a neat help target, which simply parses the comments preceded by ## to provide some simple help on the available targets!

Orchestrating multiple container builds

I set up one Makefile for each container, then higher up I have a simpler Makefile which I can use to build everything, or selective subsets.

all: common web
	$(MAKE) -C myapp-mysql
	$(MAKE) -C myapp-redis

web: common
	$(MAKE) -C myapp-web-base
	$(MAKE) -C myapp-web

common:
	$(MAKE) -C myapp-common

.PHONY : all web common

Pretty simple – if I want to just build the web containers, I make web, and it will first ensure the common containers are up to date before running the Makefiles for myapp-web-base followed by myapp-web

How to install make on CoreOS

If you’re using CoreOS for your development environment, you might be thinking “but I don’t have make, and no means to install it!”. Well here is a one-liner to brighten your day

docker run -ti --rm -v /opt/bin:/out ubuntu:14.04 \
  /bin/bash -c "apt-get -y install make && cp /usr/bin/make /out/make"

That just runs a temporary ubuntu container, installs make, then copies out of the container into the host /opt/bin directory.

Credits

Thanks to Guillaume Charmes for his blog post on docker Makefiles, and to Payton White for his neat one-liner for adding help to Makefiles.