tim olson

A site reliability engineer targeting all things Kubernetes. Coming from SWE I love working all parts of the stack. I specifically provide Kubernetes as a platform along with the administration, onboarding, and life-cycle of applications. Long live Linux.

St. Paul, MN

Makefiles for simple Docker apps

Tue, Dec 10, 2019

Estimated reading time: 5 min

docker make development

The principle purpose of this blog post is to serve as a first post for my website. Some of what I’m doing here may seem a bit vague as this is a quick an dirty explanation to use while I test my blog post model. I’d like this to be more thorough but I need to start somewhere.

With that out of the way, let’s get to it.

What/who is this for? #

I’ve often found that I’m working on projects where I need to create a small service.

  • This might be just a single Docker container developed locally
  • Or a larger project developed locally using multiple containers and compose.
  • Almost always the app is built locally and pushed to a registry. No CICD for these. (but you could)
  • Deployed to either a Docker host (i.e. special exporters) but most often Kuberentes.

A sample application #

I don’t have very many good public examples of this but an older version of this concept can be found here: https://github.com/tolson-vkn/env-echo/blob/master/Makefile

When you first enter this application you can run make:

$ make
+------------------+
| env-echo targets |
+------------------+
build        Build env-echo locally
dev          Start dev env-echo container
shell        Start a /bin/bash shell on env-echo container
up           Start immutable env-echo container
exec         Exec /bin/bash into running env-echo
watch        Start a watch command
login        Log into registry
publish      Build locally and publish to registry
deploy       Deploy env-echo to Kuberentes

You’re presented with some helpful command to get you started. I always make help text be the first thing. When using these Makefiles we aren’t building something in C, what we might use make for could be in doubt.

Before we get into the output let’s look at the start of the Makefile

TAG=$(shell git log --pretty=format:'%h' -n 1)
REGISTRY=timmyolson/env-echo
PROVIDER=docker.io
MAKE_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))

Here are some helpers defined. The first, tag is our image tag. For this repo we aren’t using semantic versioning. The command uses git log to to turn a commit into 90c376c. We will use this when we push our images to the registry.

MAKE_DIR is also worth pointing out, docker likes full paths when run from the CLI and using volume mounts; this is not ok -v ~/mydir:/opt so MAKE_DIR gives us a nice way to get the absolute path.

Ok back to the make output: The help is generated from this beauty of shell

@grep -E '^[a-zA-Z\-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'

Clearly this build our table but how do we consume it?

 .PHONY: publish
 publish: ## Build locally and publish to registry

In the example for pushing our images to our registry, clearly publish is left most, but whenever we see a recipe and ## we take the recipe name and whatever is rightmost ##. If you want hidden recipes or helpers, just leave it out.

A larger example:

.PHONY: dev
dev: ## Start dev env-echo container
 
	docker run --rm -i -t \
		--name env-echo \
		-v $(MAKE_DIR)/flask/app:/app \
		-v $(MAKE_DIR)/flask/requirements.txt:/requirements.txt \
		-e DEBUG=True \
		-p 5000:5000 \
		env-echo 

This is what we will use to mount our code into the container and start the app. Like normal CLI we’re ready to hack. The Makefile is valuable because we don’t have to remember the command to run.

I can already hear you saying; “What’s the value this delivers that docker-compose doesn’t have?”

At the surface it appears there is no advantage. And I will concede that larger projects end up looking like:

.PHONY: dev
dev: ## Start dev env-echo container

    docker-compose up

Where there is power is that;

  • What if you had multiple `docker-compose` environments a single `make dev` can find them all through make hierarchy. Think git submodules that container compose for the frontend, backend. We can bring it up easy with `make dev`
  • Sometimes `docker-compose`'s naming schemes around containers and volumes for small projects is cumbersome
  • What if you want to daemonize `docker-compose` but foreground logs
    docker-compose up -d
    docker-compose logs -f 

Other examples #

Anyways back to other recipes. Sometimes it doesn’t pay to use CICD tooling for applications. My toolbox image is a perfect example. So the make publish works well.

.PHONY: publish
publish: ## Build locally and publish to registry

        @printf '\033[33mBuild and Push ubuntools\033[0m\n';
        docker build -t ubuntools:$(TAG) ./flask
        docker tag ubuntools:$(TAG) $(REGISTRY):$(TAG)
        docker push $(REGISTRY):$(TAG)

        @printf '\033[33mUpdate the latest tag to $(TAG)\033[0m\n';
        docker tag ubuntools:$(TAG) $(REGISTRY):latest
        docker push $(REGISTRY):latest

As a final example. Not all of our development work with docker is just starting and stopping containers. Let’s say you wanted to have a handy way to connect to a containers postgres, but you don’t know the schema, etc.

.PHONY: psql
psql: ## Connect host psql to running container
ifeq ($(shell which psql &> /dev/null && echo installed), installed)
psql -U postgres -h localhost -p 5432 db
else
@printf '\033[31mError: psql not installed locally!\033[0m\n'
endif

I added a bit of flair here, this one has something different. I’d trust that users of my repo have docker installed but they might night have the psql binary. So I check it with ifeq. This is quite simply a string comparison tool in make that is rather primitive. We run the command which psql &> /dev/null && echo installed which if we don’t have the binary it short circuits and the condition is false, or if the string is installed; true.

In summary, I’m not a madman, no I don’t do this all the time. I understand there are times for sensible tools like docker-compose. But sometimes when I need a basic repo for a small app or a hacking project, I really enjoy using make to drive the environment. Hopefully you learned something.