Thoughts on layered image build with docker
Docker is an amazing abstraction on how we can put resources and environment configuration in controlled scopes.
With that in mind, for some time now, I’ve been using docker containers to run various gui applications that I didn’t wanted to install on my host machine (on my personal machine I use Ubuntu LTS).
Being able to do that is already an amazing thing, to jail application in a container and have fully control of it, though I’ve had some problems on image building dependencies.
For example, I would have this snippet to install nvidia drivers and interface libs, so I would copy that snippet on all images for gui applications image build.
That was a very naive approach that worked. Though I needed to improve it, because rebuilding the images was taking too long. I needed a dependency to rebuild images for environments.
There were some options to perform what I wanted:
Multistage build
For multistage build, on the same Dockerfile you are able to set a dependency build using multistage. That will help if you need to make some environment setup to build some artifact and then export it and bake a smaller image.
Example (source snippet from docker docs):
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]
The image generated from this is the perfect solution for golang builds as we will not need the golang runtime inside the image and will only need the output binary.
This is an improvement, though still not what I wanted.
Dockerfile –build-arg
Docker allows you to define build time arguments to provide configuration for your build images. You can use it to customize the docker build and keep it dynamic.
So, what do you think if we take a look on something like this:
ARG BASE_IMAGE
FROM $BASE_IMAGE
Exactly! You can build your image from a dynamic configuration. By using that, we can revamp the build process for docker images.
Imagine the context where you have different Dockerfile to create environment images for: golang, java, php, node, etc. Then you can make layer iteration and build it over the previous build.
The solution that worked for me involved build-args and Makefiles
Docker layered solution with build-arg and Makefile
The Gnu Make is almost omnipresent in linux environments and will allow us to build rules and dependencies to control our build.
Our abstraction will work like this:
root dir
|_ Makefile
| |_layer
| | |_golang
| | | |_Dockerfile
| | | |_Makefile
| | |_java
| | | |_Dockerfile
| | | |_Makefile
| | |_php
| | | |_Dockerfile
| | | |_Makefile
| |_utils
| |_EnvVars.mk
With that folder structure, we will able to define sub-rules on each Makefile. For this solution, we tried to keep code copy at a minimum and we have extracted the common rules into the util EnvVars.mk file.
Let’s take a look on the EnvVars.mk file:
REPO ?= 10.10.0.1:5000
REPO_PUSH ?= n
ifeq ($(BASE_IMAGE),)
IMAGE = $(REPO)/$(PROJECT):$(TAG)
else
IMAGE = $(BASE_IMAGE)-$(PROJECT)$(TAG)
BUILD_ARG_BASE_IMAGE = --build-arg BASE_IMAGE=${BASE_IMAGE}
endif
ifneq ($(REPO),)
BUILD_ARG_REPO = --build-arg REPO=$(REPO)
endif
all:
@echo "Available targets:"
@echo ""
@echo "In case you want to push the image to remote, please, define:"
@echo " REPO_PUSH=y"
@echo ""
@echo " * build - build a Docker image for $(IMAGE)"
@echo " * save - export the docker image"
@echo " * test - run a bash for the image"
@echo " * send-do - send the exported image to do and import it there"
.PHONY: build
build: Dockerfile
docker build -t $(IMAGE) \
$(BUILD_ARG_REPO) $(BUILD_ARG_BASE_IMAGE) \
.
if [ $(REPO_PUSH) = "y" ]; then \
docker push $(IMAGE); \
fi
Let’s understand what’s happening:
- At the beginning, we do some checks do define the IMAGE name and BUILD_ARG_REPO and BUILD_ARG_BASE_IMAGE
- In case BASE_IMAGE is provided, we will pass it as a config to the Dockerfile at build time and will append our docker image name and tag to the base image, to make it easier to identify
For the next file, lets take a look on the root Makefile file:
BASE_IMAGE = 10.10.0.1:5000/ubuntu20.04
NVIDIA_IMAGE = $(BASE_IMAGE)-nvidia470
.PHONY: nvidia
nvidia:
BASE_IMAGE=$(BASE_IMAGE) \
REPO_PUSH=y \
make -C gui/nvidia build
.PHONY: java-ui
java-ui: nvidia
BASE_IMAGE=$(NVIDIA_IMAGE) \
REPO_PUSH=y \
make -C layers/java build
.PHONY: jetbrains-idea
jetbrains-idea: java-ui
BASE_IMAGE=$(NVIDIA_IMAGE)-java8 \
REPO_PUSH=y \
make -C gui/jetbrains/idea-ce build
In this file we define the build dependency between images and as you can see, building the image jetbrains-idea will trigger the dependency calls.
For the last step, let’s take a look on an example image build:
Java image Dockerfile:
ARG BASE_IMAGE
FROM $BASE_IMAGE
ENV DEBIAN_FRONTEND noninteractive
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
openjdk-8-jdk openjdk-8-jdk-headless \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Java image Makefile:
include ../../utils/EnvVars.mk
PROJECT ?= java
TAG ?= 8
With these steps, I was able to achieve what I wanted (at least for now) in terms of dependency docker image build with a minimum of automation.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
10.10.0.1:5000/ubuntu20.04-nvidia470-nodejs16-webstorm2021.3.3 latest 18848f100d1a 13 hours ago 3.84GB
10.10.0.1:5000/ubuntu20.04-nvidia470-nodejs16 latest 7e8d98a3d833 13 hours ago 2.43GB
10.10.0.1:5000/ubuntu20.04-nvidia470-go1.17-goland2021.2.3 latest c9a2069d0d5a 16 hours ago 4.13GB
10.10.0.1:5000/ubuntu20.04-nvidia470-go1.17 latest b841a419c134 16 hours ago 2.74GB
10.10.0.1:5000/ubuntu20.04-nvidia470-java8-idea.ce2021.3.2 latest 7233e95dbacd 16 hours ago 4.77GB
10.10.0.1:5000/ubuntu20.04-nvidia470-java8 latest 01189dbfab29 16 hours ago 2.5GB
10.10.0.1:5000/ubuntu20.04-nvidia470 latest 452c8f71a46a 16 hours ago 2.33GB
10.10.0.1:5000/ubuntu20.04 latest 825d55fb6340 2 days ago 72.8MB
Let me know what you think. Thank you for reading it.