Base Docker Image
Concept
Docker is fantastic and has given developers more power and control over their application’s OS. Sometimes building Docker images can be slow, though. Docker layer caching technology helps speed up the process by only rebuilding required layers. But sometimes, one minor change results in having to rebuild many layers.
The slowness is exacerbated when using CI Build systems like AWS CodeBuild, GitHub Actions, CircleCI, etc to build Docker images. This is because managed build services usually create a fresh build machine to ensure consistency. It’s difficult for these services to leverage Docker layer caching effectively.
For example, even though AWS CodeBuild has a LOCAL_DOCKER_LAYER_CACHE option, the cache is a best effort and won’t be used if your builds don’t run often enough. IE: Within 15 mins or so. See: GitHub Issue 194 Local docker layer cache lifespan is too short. Similarly, CircleCI has a setup_remote_docker feature that tries to speed up the build process. It also has its own limitations with cache misses.
Base Image Approach
UFO supports building a Docker base image to help speed up the build process. The ufo docker base
commands builds a Docker image from Dockerfile.base
. It can then be used as a guaranteed cache in the FROM instruction for the main Dockerfile
. This base Docker cache image technique can substantially speeds up the build process.
Pros and Cons
There are pros and cons to using this approach. As the adage goes, there are 2 hard problems in computer science:
- Naming
- Caching
The main con about this approach is if you forget to update the base Docker image, you will have cached artifacts that will not disappear unless you rebuild the base Docker image. While some folks are not keen on this cache layer, some have loved how much it speeds up their Docker workflow. If you use this technique, you should probably set up automation that rebuilds the base Docker image on a scheduled basis.
Dynamic Dockerfile
For the case of building a new base Docker image, UFO supports dynamically creating a Dockerfile from a Dockerfile.erb
.
Why?
You may want a different FROM
statement in your Dockerfile on a per-environment basis. For example, you’ve segregated your environments like dev and prod on separate AWS accounts for security. The Docker FROM
statements could have different ECR repos from different AWS accounts.
You could also allow different AWS accounts like prod to read ECR images from the dev AWS accounts. While some are okay with this, many prefer strict security boundary permissions between the AWS accounts.
How It Works
If Dockerfile.erb
exists, UFO uses it to generate a Dockerfile
as a part of the build process. Here’s what the FROM
statement in a Dockerfile.erb
looks like:
Dockerfile.erb
FROM <%= @base_image %>
The @base_image
variable is read from state stored by UFO. This state can be stored on s3. See: Config State Docs
The ufo docker base command automates the process, including updating the state data and updating the existing Dockerfile FROM statement.
When UFO_ENV=dev
, it’ll produce the following.
Dockerfile
FROM 1111111111111.dkr.ecr.us-west-1.amazonaws.com/demo/base:base-2022-15-10T03-23-34-foobarabc
When UFO_ENV=prod
, it’ll produce the following.
Dockerfile
FROM 222222222222.dkr.ecr.us-west-1.amazonaws.com/demo/base:base-2022-16-10T03-23-34-foobarxzy
Usage
The general steps are:
- Create both
Dockerfile.base
andDockerfile.erb
with<%= @base_image %>
. RemoveDockerfile
and add it to.gitignore
. - Run:
ufo docker base
to update the state with the built Docker base image. Probably should be automated on a scheduled basis. - Run:
ufo ship
to build a Docker image usingDockerfile.erb
.
Important: Remember when using Dockerfile.erb
, you should update the source Dockerfile.erb
instead of Dockerfile
. The Dockerfile
is auto-generated. You should .gitignore
the Dockerfile
.
Build Args
Why not use build args?
UFO automates this process, so users will not have to remember to provide the build arg. Found that it is too easy to forget to specify the build args, and there are still a lot of manual steps aside from the use of build args.