Skip to content

Building the images

Most people should just pull the published images. Build them yourself when you want to change tool versions, add or remove tools, or produce images for your own registry.

  • Pull if…


    • the standard toolset is enough
    • you want multi-arch images that are rebuilt automatically
    • you don't want a long compile on every machine
    docker pull ghcr.io/jinalshah/devops/images/all-devops:latest
    
  • Build if…


    • you need different tool or Python versions
    • you want to add internal tools or remove some
    • you publish to your own registry

    Or extend the published image

How the Dockerfile is organised

One Dockerfile with a shared base stage and three targets. Each target is a single stage on top of base; there's no separate builder stage.

flowchart TB
  R["rockylinux/rockylinux:10"] --> S1["base: RUN 1<br/>dnf packages, gh, DB clients, Trivy,<br/>Python compiled from source, pip tools, Oh My Zsh"]
  S1 --> S2["base: RUN 2<br/>kubectl, Terraform (tfswitch), Terragrunt,<br/>TFLint, Packer, Helm, ghorg, k9s, Task"]
  S2 --> S3["base: RUN 3<br/>Node.js LTS, Claude Code, Codex,<br/>Copilot CLI, Antigravity CLI"]
  S3 --> A["all-devops<br/>+ AWS + gcloud"]
  S3 --> W["aws-devops<br/>+ AWS"]
  S3 --> G["gcp-devops<br/>+ gcloud"]

  classDef neutral fill:#334155,stroke:#1e293b,color:#fff
  classDef base fill:#0891b2,stroke:#0e7490,color:#fff
  classDef all fill:#059669,stroke:#047857,color:#fff
  classDef aws fill:#ea7a0c,stroke:#c2410c,color:#fff
  classDef gcp fill:#2563eb,stroke:#1d4ed8,color:#fff
  class R neutral
  class S1,S2,S3 base
  class A all
  class W aws
  class G gcp

Because the three targets share base, building a second target after the first reuses the cached base layers and only runs the small cloud layer.

Prerequisites

  • Docker with BuildKit, the default builder since Docker Engine 23 and in Docker Desktop. docker buildx version confirms it.
  • Plenty of disk space: the finished image is about 5 GB unpacked, and the build cache adds more on top.
  • A clone of the repository: git clone https://github.com/jinalshah/devops-images && cd devops-images

Cold builds are slow

Python is compiled from source with --enable-optimizations, which dominates a cold build. Expect a cold build to take tens of minutes; later builds are much faster once the base layers are cached.

Build a target

docker build --target all-devops -t all-devops:local .
docker build --target aws-devops -t aws-devops:local .
docker build --target gcp-devops -t gcp-devops:local .
docker build --target base -t devops-base:local .

base isn't published, but it's handy for testing shared tooling.

To build every target in turn (the base is built once and reused):

for target in all-devops aws-devops gcp-devops; do
  docker build --target "$target" -t "$target:local" .
done

Docker builds for your machine's architecture by default. On Apple Silicon or an ARM host you get a native linux/arm64 image. For both architectures, see Multi-platform images.

Build arguments

These are exactly the ARGs declared at the top of the Dockerfile. CI overrides the first eight from repository variables, which a daily workflow bumps to the latest releases.

Arg Default Used for
GCLOUD_VERSION 501.0.0 Google Cloud SDK tarball (all-devops, gcp-devops)
PACKER_VERSION 1.11.2 Packer
TERRAGRUNT_VERSION 0.68.14 Terragrunt
TFLINT_VERSION 0.50.3 TFLint
GHORG_VERSION 1.9.10 ghorg
K9S_VERSION 0.32.7 k9s
PYTHON_VERSION 3.14.7 Full Python version compiled from source
PYTHON_VERSION_TO_USE python3.14 Interpreter registered as the default python3; must match PYTHON_VERSION
MONGODB_VERSION 8.0 MongoDB repository series for mongosh
MONGODB_REPO_PATH /etc/yum.repos.d/mongodb-org-${MONGODB_VERSION}.repo Where the MongoDB repo file is written
MYSQL_RELEASE_RPM_URL https://repo.mysql.com/mysql84-community-release-el10-3.noarch.rpm MySQL 8.4 community repository for the mysql client
MYSQL_GPG_KEY_URL https://repo.mysql.com/RPM-GPG-KEY-mysql-2025 Current MySQL signing key

Terraform, kubectl, Helm, Task, Node.js and the AI CLIs have no build arg; they always install the latest release at build time.

docker build --target all-devops \
  --build-arg TERRAGRUNT_VERSION=0.68.14 \
  --build-arg K9S_VERSION=0.32.7 \
  -t all-devops:custom .

You need both the full version (for the source download) and the matching binary name. PYTHON_VERSION=3.13 on its own breaks the build.

docker build --target all-devops \
  --build-arg PYTHON_VERSION=3.13.7 \
  --build-arg PYTHON_VERSION_TO_USE=python3.13 \
  -t all-devops:py313 .

Keep versions in one file and pass them through. --build-arg NAME with no value takes the value from your environment.

versions.env
GCLOUD_VERSION=501.0.0
PACKER_VERSION=1.11.2
TERRAGRUNT_VERSION=0.68.14
TFLINT_VERSION=0.50.3
GHORG_VERSION=1.9.10
K9S_VERSION=0.32.7
set -a; . ./versions.env; set +a
docker build --target all-devops \
  --build-arg GCLOUD_VERSION --build-arg PACKER_VERSION \
  --build-arg TERRAGRUNT_VERSION --build-arg TFLINT_VERSION \
  --build-arg GHORG_VERSION --build-arg K9S_VERSION \
  -t all-devops:pinned .

Validate the result

validate-build.sh
#!/usr/bin/env bash
set -euo pipefail
IMAGE="$1"

docker run --rm "$IMAGE" bash -c '
  set -e
  terraform version
  terragrunt --version
  kubectl version --client
  helm version --short
  ansible --version | head -1
  trivy --version | head -1
  python3 --version
  node --version
  command -v claude codex copilot agy
'

# Cloud CLIs depend on the target
docker run --rm "$IMAGE" aws --version 2>/dev/null && echo "AWS CLI: present"
docker run --rm "$IMAGE" gcloud --version 2>/dev/null | head -1 || true
./validate-build.sh all-devops:local

The repository also has test scripts for the network tools. Each takes the image as its first argument:

./test_network_tools.sh all-devops:local
./test_dns_tools.sh all-devops:local
./test_ncat_tool.sh all-devops:local

Speed up rebuilds

Build targets one after another on the same builder. BuildKit reuses the cached base layers, so only the cloud layer runs for the second and third targets.

With a docker-container buildx builder you can export the cache and reuse it later, or on another machine:

docker buildx create --name devops --driver docker-container --use
docker buildx build --target all-devops \
  --cache-from type=local,src=.buildx-cache \
  --cache-to type=local,dest=.buildx-cache,mode=max \
  -t all-devops:local --load .

The project's own workflow uses the GitHub Actions cache backend per target and architecture:

cache-from: type=gha,scope=all-devops-amd64
cache-to: type=gha,mode=max,scope=all-devops-amd64

The published images can't seed your cache

The published images are built with the GitHub Actions cache backend and carry no inline cache metadata, so --cache-from ghcr.io/jinalshah/devops/images/all-devops:latest won't speed up your build.

To shrink an image, see the Optimisation guide.

Troubleshooting

No space left on device

Free up space with docker builder prune (build cache) and docker image prune -a. On Docker Desktop, raise the disk limit under Settings → Resources.

The Python step fails

Check that PYTHON_VERSION is a full release that exists on python.org (for example 3.13.7, not 3.13), and that PYTHON_VERSION_TO_USE names the same minor version (python3.13).

A download returns 404

A pinned version probably doesn't exist, or doesn't publish a build for your architecture. Check the release page for the tool, then pass a different --build-arg, for example --build-arg GCLOUD_VERSION=<a version from the SDK release notes>.

Repository mirrors are slow or unreachable

Rocky Linux 10's repo definitions are /etc/yum.repos.d/rocky*.repo. To use an internal mirror in a fork of the Dockerfile:

RUN sed -i -e 's|^mirrorlist=|#mirrorlist=|' \
           -e 's|^#baseurl=http://dl.rockylinux.org|baseurl=https://mirror.example.com|' \
           /etc/yum.repos.d/rocky*.repo
Building amd64 on Apple Silicon is very slow

Cross-architecture builds run under emulation. Build linux/arm64 locally, or build each architecture on a native machine as CI does. See Multi-platform images.

Next steps