Skip to content

Containers

Nearly every service the DevOps Division ships runs as a container, and the images those containers run from are the single most important supply-chain surface we curate. This page explains how we approach containers as a division. It covers the platform we build and run on, the base images we publish as part of the Unified DevOps Platform, and how the pieces fit alongside our DevSecOps continuous delivery standard.

The container platform we use

We use Docker exclusively. Engineers are provided a licensed install of Docker Desktop, and our GitLab CI/CD pipelines are built around Docker-in-Docker so the same image build and run tooling is used locally and in CI.

We publish images as multi-architecture manifests covering linux/amd64 and linux/arm64. That means an engineer developing on an Apple silicon laptop pulls the arm64 variant of the same image reference that CI and our production runtimes pull as amd64. Local development, CI, and production stay in step without maintaining parallel tags.

We do not currently support alternative container runtimes such as Podman or containerd for internal use. Standardising on Docker keeps our tooling, examples, and support surface small.

Base images and tooling images

Two related but distinct kinds of image show up throughout our processes, and it's worth naming them before the rest of this page relies on the distinction.

A base image is a minimal operating system image, for example a debian-slim derivative, that contains only the essential system components and packages needed to support downstream workloads. It has essential system configuration and security policies applied on top, such as a non-root USER instruction in its Dockerfile. It may optionally include a core language runtime such as Node.js or Python, but it does not contain application-level packages. npm-installed and pip-installed dependencies belong to the application image, not to its base.

Base images are long-lived, versioned, and curated centrally so that every application built against them inherits the same posture. A Python service image should start FROM one of our base images not from an upstream python:* tag directly.

A tooling image is an ephemeral image that packages a task-specific toolchain for use in CI jobs or from an engineer's shell. Our gcloud-docker, logan-terraform, and ansible-playbook images are examples. Tooling images are not intended as foundations for downstream application images. Their job is to provide a consistent environment for a single task.

The rest of this page is mostly about base images. Tooling images follow the same publishing conventions but are used differently.

Docker Hardened Images

Our base images are built on Docker Hardened Images (DHI), a Docker product that provides minimal, distroless base images with a strong security posture. We adopted DHI as our foundation because it gives us three properties we would otherwise have had to build and maintain ourselves.

DHI images are distroless by design. They contain the runtime a workload needs and very little else. There is no shell, no package manager, and no general-purpose userland in a production DHI image. That eliminates whole classes of both attack surface and CVE noise, at the cost of requiring a modern container build workflow.

DHI images are patched aggressively upstream. Docker rebuilds them on their own cadence in response to upstream security updates, so we inherit patched images without operating a bespoke rebuild pipeline of our own for the same purpose.

DHI images ship with very few known CVEs by default. That reduces the routine background noise our vulnerability triage process has to filter, so genuine findings surface faster.

Our DHI base image collection

We publish an approved collection of DHI-derived base images from the uis/devops/platform/base-images GitLab group. This collection is part of the Unified DevOps Platform and is the recommended starting point for any new container image built in the division.

Each image in the collection is a thin layer over an upstream DHI base, adding only the minimal UIS DevOps configuration and policy we want every project to inherit. We rebuild the collection nightly, so downstream projects pick up the latest upstream DHI patches on their next Renovate digest bump.

We ship two variants of every base image.

The main base variant is distroless and runs as a non-root user. It is intended for the final runtime stage of a Dockerfile, where a project's built artefacts and their runtime are all that should be present.

The dev variant includes a shell and a package manager and runs as root. It is intended for the build stages of a Dockerfile, where installing dependencies and compiling artefacts require those tools.

Because the base variant is distroless, a multi-stage Dockerfile is not optional. It is the only way to consume our base images. This aligns with the multi-stage build and non-root runtime requirements already set out in the DevSecOps continuous delivery standard.

All base images in the collection are published as multi-architecture manifests covering linux/amd64 and linux/arm64.

Why Debian, not Alpine

We publish our DHI base image collection on Debian only. Every image in platform/base-images is derived from a debian-slim DHI variant. We do not publish Alpine-based variants and do not intend to support Alpine as a base for platform images.

The primary reason is standard C library compatibility. Debian uses glibc, and the wider language ecosystem is built and tested against glibc as its default target. Python's manylinux wheels, Node.js native modules, and prebuilt Go binaries linked against C libraries all assume glibc at runtime. Alpine uses musl, which is a different C library implementation. On Alpine, projects either compile native dependencies from source at image build time, which is slow and requires an additional build toolchain, or they rely on musl-specific wheels which lag behind their glibc equivalents. Standardising the division on Debian removes that class of friction from every project's build.

The obvious trade-off is image size. An Alpine base is smaller than a Debian one. However, DHI already ships a very minimal userland on top of debian-slim, so the practical difference is much narrower than headline image sizes suggest. We consider ecosystem consistency to be worth the modest size cost.

The legacy dockerimages registry

Prior to the DHI base image collection, our base and tooling images were published from uis/devops/infra/dockerimages. That registry hosted images including python-alpine, gcloud-docker, logan-terraform, and ansible-playbook, and is referenced from a number of existing pages in this guidebook.

We are in the process of deprecating dockerimages in favour of the new platform/base-images group. During the transition the existing lifecycle rules in the container image deprecation policy continue to apply. New projects should start from the platform/base-images collection.

How this fits with the rest of the platform

Container work in the division touches several other pieces of our platform.

The DevSecOps continuous delivery standard is authoritative on what a compliant container image looks like. It already requires multi-stage builds, non-root runtime users, and fresh base images. The DHI base image collection is the mechanism by which projects satisfy those requirements without having to reinvent them.

Renovatebot keeps the digest references in our Dockerfiles current. The keeping Docker image references fresh how-to describes the configuration, and applies equally to references pointing at the new base image collection.

Any CVE surfaced against one of our base images, or against a project image built on top of one, follows the vulnerability triage process. The reduced CVE surface of DHI images makes that queue shorter, not empty.

Summary

We run our workloads on containers built from a curated collection of Docker Hardened Image based images published at uis/devops/platform/base-images. Every image ships as a dev and base variant across amd64 and arm64, and consuming the base variant requires a multi-stage Dockerfile. Tooling images continue to be a separate, task-focused kind of image that we build in the same way but use differently. The legacy dockerimages registry is being deprecated in favour of this new collection.

See also