Container images

SlowShield serves container images at https://slowshield.example.com/v2/, from Docker Hub, GHCR, Quay, registry.k8s.io, gcr.io, MCR and ECR Public. Tags lag behind: FROM nginx:latest keeps working, about a week behind.

Set it up

Each runtime has its own setting. These were checked against containerd 2.2, Docker 29.8, Podman 5.8, BuildKit 0.33, skopeo and crane: with them, containerd, Docker's default image store and Podman only ever ask SlowShield.

containerd

/etc/containerd/certs.d/_default/hosts.toml: every registry, pull only

server = "https://slowshield.example.com"
capabilities = ["pull", "resolve"]

/etc/containerd/certs.d/ghcr.io/hosts.toml: a registry you also push to

server = "https://ghcr.io"
capabilities = ["push"]

[host."https://slowshield.example.com"]
  capabilities = ["pull", "resolve"]

Docker

/etc/docker/certs.d/_default/hosts.toml: the containerd image store (docker info lists io.containerd.snapshotter.v1), for docker pull and docker build

server = "https://slowshield.example.com"
capabilities = ["pull", "resolve"]

/etc/docker/daemon.json: the classic image store. Docker Hub only, and Docker pulls from Docker Hub itself after a refusal

{
  "registry-mirrors": ["https://slowshield.example.com"]
}

Podman

/etc/containers/registries.conf.d/50-slowshield.conf: Podman, CRI-O, Buildah and skopeo

[[registry]]
prefix = "docker.io"
location = "slowshield.example.com/docker.io"

[[registry]]
prefix = "gcr.io"
location = "slowshield.example.com/gcr.io"

[[registry]]
prefix = "ghcr.io"
location = "slowshield.example.com/ghcr.io"

[[registry]]
prefix = "mcr.microsoft.com"
location = "slowshield.example.com/mcr.microsoft.com"

[[registry]]
prefix = "public.ecr.aws"
location = "slowshield.example.com/public.ecr.aws"

[[registry]]
prefix = "quay.io"
location = "slowshield.example.com/quay.io"

[[registry]]
prefix = "registry.k8s.io"
location = "slowshield.example.com/registry.k8s.io"

BuildKit

buildkitd.toml (also docker buildx create --buildkitd-config): BuildKit pulls from the registry itself after a refusal

[registry."docker.io"]
  mirrors = ["slowshield.example.com"]
[registry."gcr.io"]
  mirrors = ["slowshield.example.com"]
[registry."ghcr.io"]
  mirrors = ["slowshield.example.com"]
[registry."mcr.microsoft.com"]
  mirrors = ["slowshield.example.com"]
[registry."public.ecr.aws"]
  mirrors = ["slowshield.example.com"]
[registry."quay.io"]
  mirrors = ["slowshield.example.com"]
[registry."registry.k8s.io"]
  mirrors = ["slowshield.example.com"]

Image names

any client: SlowShield's host in front of the image name

crane pull slowshield.example.com/docker.io/library/nginx:1.29 nginx.tar

Dockerfile

FROM slowshield.example.com/docker.io/library/nginx:1.29

Docker Desktop (macOS, Windows)

Docker Desktop's engine runs in a VM. When Docker Desktop starts, it copies ~/.docker/certs.d on your machine to /etc/docker/certs.d in the VM, so put the file there and restart Docker Desktop:

mkdir -p ~/.docker/certs.d/_default
cat > ~/.docker/certs.d/_default/hosts.toml <<'EOF'
server = "https://slowshield.example.com"
capabilities = ["pull", "resolve"]
EOF

From then on every docker pull and every base image of docker build comes through SlowShield. If SlowShield is unreachable, pulls fail instead of going to the registry. Pushes need the registry's own file (see containerd above).

Podman on macOS and Windows

Podman runs in a VM too. Put the drop-in there:

podman machine ssh 'mkdir -p ~/.config/containers/registries.conf.d && cat > ~/.config/containers/registries.conf.d/50-slowshield.conf' < 50-slowshield.conf

(/etc/containers/registries.conf.d/ for a rootful machine.) Short names like python:3.13-slim resolve to docker.io/library/python first, and the drop-in then sends them to SlowShield.

Kubernetes

The kubelet pulls through containerd, which reads the hosts.toml files only when its CRI plugin's config_path names the directory. The default is empty:

# /etc/containerd/config.toml (containerd 2.x)
[plugins.'io.containerd.cri.v1.images'.registry]
  config_path = "/etc/containerd/certs.d"

In containerd 1.7 the section is [plugins."io.containerd.grpc.v1.cri".registry]. Check a node with containerd config dump | grep config_path. A pod whose image is held stays in ImagePullBackOff and starts by itself once the hold ends.

What changes

Every refusal is 403 with a message. What the clients print:

ClientOutput
Docker (containerd image store)Error response from daemon: error from registry: slowshield: docker.io/library/brandnew:latest (sha256:162a60de2ed8…) is too new. It was pushed …; this proxy requires 7 days. It becomes available at …
docker build, BuildKit403 Forbidden, then denied: slowshield: … is too new …
Podman, skopeoreading manifest latest in slowshield.example.com/docker.io/library/brandnew: denied: slowshield: …
craneDENIED: slowshield: …
containerd 2.2 and older (ctr, nerdctl, Kubernetes events)… 403 Forbidden, without the message

The second layer

No container runtime has a release-age setting, so SlowShield is the only layer.

What can go around it

To build images with SlowShield for the base images and every dependency, see Building images.

For agents: this page as Markdown · llms.txt · the SlowShield skill