Easy, reproducible, and shareable development environments
March 18, 2020
Docker makes it easy to create container images containing the tools you need to work on a project. However this can be fiddly to use well in many circumstances. Here we will visit the normal approach of using a docker container for development environments, indicate some ways in which it gets complicated, and then introduce a solution, called floki which greases the wheels and makes the whole process easier.
Creating a basic build environment with Docker
Let’s imagine we have a little C project which we want to statically compile using musl libc. We might, in the K&R tradition, have
#include <stdio.h>
int
main(int argc, char* argv[])
{
printf("Hello, world\n");
return 0;
}
in a file main.c
, and a simple Makefile
.PHONY: clean
all:
gcc -o main -static main.c
clean:
rm main
To build against musl libc, it can be convenient to use a docker container built off of an alpine image as a base, where make
and a C compiler have been installed. We can declare this in a Dockerfile
FROM alpine:latest
RUN apk update && apk add alpine-sdk
(in practice, using the latest
tag and running apk update
is unlikely to be reproducible - you’ll want to pin your image more precisely).
This can be built with
$ docker build -t hello-world .
And we can mount our codebase at /mnt
inside a container running this image with
$ docker run --rm -it -v $(pwd):/mnt hello-world:latest
From here, you should be able to cd /mnt
and run make
to get a statically linked binary linked against musl libc.
Great! This is a fine way to get working with docker as a build environment. However, it’s a bit of a pain to run the build and then run steps when the needs of the build container change. This is of course scriptable, but repeating this pattern across many codebases leads to a lot of copy-pasting of scripts. Furthermore, if you need additional settings for your docker container - additional volume mounts, docker-in-docker for testing with e.g. docker-compose
, or forwarding of an SSH agent to authenticate with a git
server, the scripting becomes more unwieldy, and harder to maintain across codebases.
Oftentimes, build environments are neglected, and even if developers are using docker containers for their build, they don’t share anything to replicate that environment for other users. Figuring out what is needed to build a project is an unnecessary time sink.
floki
floki
essentially lets you write what you want in a YAML file, and turns that into reality. It was created to solve the problems above, which I found across multiple microservice codebases. It’s great for new developers wanting to get build environments, because they just need to run floki
and they are good to go.
So how would the example above translate?
Well, we keep the Dockerfile
, and then we create a file called floki.yaml
in the root of our codebase:
image:
build:
name: hello-world
dockerfile: Dockerfile
mount: /mnt
init:
- echo 'Welcome to the hello-world build container'
We can then simply run floki
from a shell, and floki
takes care of building the docker image, and dropping us into the shell in the mounted working directory. It even prints out the friendly greeting from the init
section - in practice it’s nice to print some basic usage instructions here, for example, instructions for how to build a project, or run tests.
More wins
Although this seems small, the ergonomics are already much better. Regardless, floki
gives us more for free.
docker-in-docker
If our docker container has the docker command line tools (or some other way to interact with a docker daemon), we can get docker-in-docker support by adding
dind: true
to our floki.yaml
.
SSH agent forwarding
floki
lets you forward your SSH agent. This can be useful if you want to pull libraries from a private git
server for a build, or if you want to configure a dockerized environment for SSHing into virtual machines (maybe you want to run fabric
or ansible
from inside the container).
You can enable this by adding
forward_ssh_agent: true
to your floki.yaml
.
Build-caches with floki
volumes
Losing cached build artifacts between runs of build containers is annoying, especially if you have to compile them from source. floki
lets you attach volumes (which can even be shared among different containers) to use as a build cache.
Here is an example for caching Rust artifacts
image: ekidd/rust-musl-builder
mount: /home/rust/src
forward_ssh_agent: true
shell: bash
volumes:
registry:
mount: /home/rust/registry
By default, each the volume is localised to a particular project (that is, the folder and filename for the floki
configuration file). The shared: true
key can be added to the volume
key to use the same volume across all other projects with the same volume name (registry
in the example above) and with the shared
key set to true. In the example above, this would mean all rust containers with the shared
registry
volume will share the same volume. Combine this with a caching solution on the backing directory and you have a cross company build cache.
Conclusion
Creating reproducible build environments makes reliable builds easier to achieve. floki makes doing this more ergonomic, and lowers the barrier to doing so, and makes it easier to share with other developers.
There is more than just the above - check out the floki GitHub page for more.