Friends Don't Let Friends apt-update in Dockerfiles
One common anti-pattern that keeps popping up is the use of package updates in Dockerfiles. A simple search on github.com reveals 1M+ results! Can we do better?
The issue
When you include apt-get update in your Dockerfile or Containerfile, you’re
essentially telling Docker to fetch the latest package information every
time you build your image. What this means in practice:
- Your image is likely ending up with different package versions each time you build it, leading to potential compatibility issues and making your builds non-deterministic.
- You’re increasing the build time unnecessarily.
- You’re adding another dependency on a network resource - your build could fail if the package repository is unavailable, even if no updates were pending.
- You work against Docker’s layer caching as the update layer will frequently change, causing all subsequent layers to be rebuilt.
What to do instead
Build a dedicated base image that contains your packages, and version it. When you need a
new set of packages, you rebuild the base image. Your downstream Dockerfiles never run apt-get update.
Create a base image
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
package1 \
package2 \
package3 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
push it to your registry
docker build -t myorg/mybase:v1 .
docker push myorg/mybase:v1
and depend on that in your application Dockerfiles…
FROM myorg/mybase:v1
COPY . /app
RUN ...
Every build is now deterministic, the package set is locked to whatever v1 contains. When you need updates, build and push v2.
Other approaches
- Pin package versions directly (
apt-get install -y curl=7.68.0-1ubuntu2), deterministic but tedious to maintain. - Use distroless or minimal base images like
gcr.io/distrolessor Alpine, fewer packages means fewer reasons to runapt-getat all. - Multi-stage builds, install build dependencies in a throwaway stage, copy only the artifacts to a clean final image.