This repository contains Docker images used by Keploy for CI/CD pipelines.
A Docker-in-Docker image with Go and common CI dependencies for running Keploy tests in CI pipelines.
Features:
- Based on
debian:trixie-slim, with Docker Engine 29.1 from Docker's apt repository (dockerd, docker CLI, buildx, compose, containerd) - Go 1.27.2, the one Go patch of every image here that has Go; no C
toolchain (Linux builds use
CGO_ENABLED=0; Windows cross-compiles usekeploy-ci-go-build) - Common CI utilities (bash, curl, git, jq, sudo, zstd, etc.); no MinIO client (see below)
- Helper script
start-dockerto start Docker daemon inside containers
Usage:
# Example in GitHub Actions. Pin a release, never `latest` (see Publishing).
container:
image: ghcr.io/keploy/keploy-ci:1.2.40
options: --privileged# Start Docker daemon inside the container
start-docker
# Docker is now ready
docker infokeploy-ci plus the Kubernetes CLIs the kube control-plane e2e lanes need, baked
in so they stop downloading them from the public internet on every run.
Adds on top of keploy-ci: kind, kubectl, helm, and mikefarah yq
(pinned — the standard versions, kept in step with keploy-ci-playwright).
Use it as a docker client against a sibling docker:dind service (point
DOCKER_HOST at the sibling). Do not run its own dockerd via start-docker:
running kind inside a nested Docker daemon is unreliable, which is why the e2e
lanes use a sibling dind.
# Woodpecker step, talking to a sibling docker:dind service
image: ghcr.io/keploy/keploy-ci:kube-1.2.40Tag: ghcr.io/keploy/keploy-ci:kube-<version>.
The toolchain keploy/ebpf's committed BPF objects are built with (the recipe in
its compile.sh header), pinned, for its objects-reproducible lane: that lane
rebuilds every committed object from scratch and fails on any byte that differs,
so the toolchain must not move under it, and the lane must not wait on an Ubuntu
mirror to install it.
Contents: Ubuntu 22.04 (by index digest), clang-14 and llvm-14
1:14.0.0-1ubuntu1.1, libbpf-dev 1:0.5.0-1ubuntu22.04.1, linux-libc-dev,
libc6-dev and their arm64 cross packages, git, ca-certificates, and Go
1.27.2 (GOTOOLCHAIN=local). Every package installed comes from one
snapshot.ubuntu.com snapshot and the recipe's packages are pinned with
=version, so a rebuild resolves the same versions or fails; Go is checked
against its SHA-256. amd64 only: the snapshot service does not serve
ubuntu-ports. It is published by a job of its own (standalone-images), not as
a base-images leg, so that it never holds the derived images back.
Use it by digest, not by tag: the lane's result is only meaningful for one fixed toolchain. Read the digest from the tag once the release's publish run has finished, not from a build log:
docker buildx imagetools inspect ghcr.io/keploy/keploy-ci:ebpf-objects-<version> --format '{{.Manifest.Digest}}'# Woodpecker step in keploy/ebpf
image: ghcr.io/keploy/keploy-ci:ebpf-objects-<version>@<digest>A version changed here can change the bytes the recipe builds, so moving the
lane to a new digest is a keploy/ebpf change of its own, which rebuilds and
recommits its objects with that image when they differ. Once Canonical stops
serving the pinned snapshot (its stated aim is at least the last two years), the
image no longer builds and the snapshot has to move, which is such a change.
Never prune a published ebpf-objects-* image from GHCR, nor an untagged
keploy-ci version a lane's digest names: re-running a publish job pushes the
tag again under a new digest and leaves the pinned one untagged, lanes pull it
by digest, and it cannot be rebuilt after that window.
Tag: ghcr.io/keploy/keploy-ci:ebpf-objects-<version>.
None of these images ships MinIO's mc any more. The CI store behind
MINIO_ENDPOINT is SeaweedFS (it replaced MinIO on 2026-09-25; the Woodpecker
secrets keep their MINIO_* names), and MinIO withdrew its distribution
(dl.min.io answers 410 Gone, the Docker Hub images are gone), so a baked mc was
a binary nobody could rebuild from upstream.
Lanes reach the store through their own repo's .ci/scripts/s3.sh
(scripts/ci/s3.sh in k8s-proxy): a POSIX-sh S3 client over the curl every
image here already has (curl >= 7.75; go-build ships 7.88.1, the rest 8.x).
It is versioned and tested with the lanes that call it, so it is not on PATH
in these images.
The one image-owned user of the store is minio-cache (node, playwright,
lighthouse; the name is historical). It speaks through the same s3.sh,
installed beside it at /usr/local/lib/keploy-ci/s3.sh and byte-identical to the
repos' copies (docker-build.yml checks every copy against the shared hash and
runs both against a SeaweedFS 4.47 container in the freshly built node image):
restoredownloads the object and verifies it (size, and MD5 where the ETag is one) beforetarsees a byte; a miss or an unreachable store is a cold start (exit 0), the latter reported on stderr.saveuploads with one PUT (Content-MD5, read back), so an object is replaced whole or not at all; a failed upload exits non-zero. Caches are limited to 5 GiB, S3's single-PUT limit.- It never creates a bucket (a new bucket on the store gets no retention) and never writes a lifecycle rule (they are locked; retention is server-side).
The images form a three-tier chain, and publish.yml builds them in that order:
| tier | images | built FROM |
|---|---|---|
| 1 | keploy-ci, slim, timefreeze, golint, awscli, azurecli, go-build; ebpf-objects (its own job) |
upstream (debian/alpine/golang/ubuntu) |
| 2 | node, java, python, kube |
keploy-ci:<version> |
| 3 | playwright, lighthouse |
keploy-ci:node-<version> |
Tiers 2 and 3 take a BASE_TAG build arg rather than hard-coding the base in
the FROM line, and the workflow passes the version being published. That is
load-bearing, not cosmetic:
Every derived Dockerfile used to pin a literal base tag, and nothing ever moved
those literals — so a derived image kept building on whatever base was current
the day it was written. keploy-ci-node sat on keploy-ci:1.2.23 for nine
releases. When the base moved to Go 1.27, node — and therefore playwright and
lighthouse, which build FROM node — silently stayed on Go 1.26. Downstream
repos then compiled go 1.27 modules inside a Go 1.26 image; because these
images default to GOTOOLCHAIN=auto that does not fail, it just downloads a
~325 MB toolchain on every cold cache.
Tier 3 is also a separate job that needs: tier 2. It used to share tier 2's
matrix and run in parallel with node, which is why playwright pinned an
already-published node tag: the node image from the same release did not exist
yet when playwright built.
The ARG BASE_TAG= default in each Dockerfile is only for a bare local
docker build; CI always overrides it.
Images are automatically published to GitHub Container Registry (ghcr.io) when
a vX.Y.Z tag is pushed (e.g., v1.0.0), once per tag. Cutting a release with
a new tag pushes it, so a release publishes too, through that push. (The
workflow used to run on release: published as well, which fired with the same
push and published every tag twice, under two digests.)
Tags follow semantic versioning:
v1.0.0→1.0.0,1.0,1for every image, prefixed per variant (node-1.0.0,playwright-1.0.0, …)
There is no <prefix>-latest. The <prefix>latest tag rule is
enable={{is_default_branch}}, which is false on the tag refs this workflow
runs on, so it never fires.
The bare latest comes from somewhere else: metadata-action's flavor, whose
default latest=auto adds latest to every type=semver rule whatever its
pattern prefix. Until publish.yml set the flavor explicitly, every leg of
every job pushed a bare latest and the last to finish won; at v1.2.40 it was
the amd64-only playwright image. Now only the base leg publishes it
(latest=auto) and every other leg sets latest=false, which docker-build.yml
checks. A latest pulled before the first release after that fix is still
the wrong image. Pin a full X.Y.Z tag; every consuming repo does.