Docker Best Practices
Docker best practices that we follow are listed here. Note that this is not an exhaustive list, but rather some of the ones that have stood out for us as practical ways of leveraging Docker together with the Cloud Posse reference architecture.
Inheritance
Inheritance is when you use FROM some-image:1.2.3
(vs FROM scratch
) in a Dockerfile
. We recommend to leverage lean base images (E.g. alpine
or busybox
).
Try to leverage the same base image in as many of your images as possible for faster docker pulls
.
Multi-stage Builds
There are two ways to leverage multi-stage builds:
- Build-time Environments The most common application of multi-stage builds is for using a build-time environment for compiling apps, and then a minimal image (E.g.
alpine
orscratch
) for distributing the resultant artifacts (e.g. statically-linkedgo
binaries). - Multiple-Inheritance We like to think of "multi-stage builds" as a mechanism for "multiple inheritance" as it relates to docker images. While not technically the same thing, using multi-stage images makes it possible to
COPY --from=other-image
to keep things very DRY.
Use Scratch Base Image
One often overlooked, ultimately lean base-image is the scratch
image. This is an empty filesystem which allows one to copy/distribute the minimal set of artifacts. For languages that can compile statically linked binaries, using the scratch
base image (e.g. FROM scratch
) is the most secure way as there will be no other exploitable packages bundled in the image.
We use this pattern for our terraform-root-modules
distribution of terraform reference architectures.
Configure Cache Storage Backends
When using BuildKit, you should configure a cache storage backend that is suitable for your build environment. Layer caching significantly speeds up builds by reusing layers from previous builds, and is enabled by default as BuildKit has a dedicated local cache. However, in a CI/CD build environment such as GitHub Actions, an external cache storage backend is essential as there is little to no persistence between builds.
Fortunately, Cloud Posse's cloudposse/github-action-docker-build-push action uses gha
(the GitHub Actions Cache) by default. Thus, even without any additional configuration, the action will automatically cache layers between builds.
When using self-hosted GitHub Actions Runners in an AWS environment, however, we recommend using ECR as a remote cache storage backend. Using ECR as the remote cache backend—especially in conjunction with a VPC endpoint for ECR—results in reduced NAT Gateway costs and faster layered cache imports when compared to the GitHub Actions Cache.
The following example demonstrates how to configure the cloudposse/github-action-docker-build-push action to use ECR as the remote cache storage backend:
- name: Build
id: build
uses: cloudposse/github-action-docker-build-push@main
with:
registry: registry.hub.docker.com
organization: "${{ github.event.repository.owner.login }}"
repository: "${{ github.event.repository.name }}"
+ cache-from: "type=registry,ref=registry.hub.docker.com/${{ github.event.repository.owner.login }}/${{ github.event.repository.name }}:cache"
+ cache-to: "mode=max,image-manifest=true,oci-mediatypes=true,type=registry,ref=registry.hub.docker.com/${{ github.event.repository.owner.login }}/${{ github.event.repository.name }}:cache"
For more information with regards to the cache-from
and cache-to
options, please refer to the docker buildx documentation.