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.