Building images
A container build fetches from two kinds of places: the registries its base images come from, and the
package registries its RUN steps install from. Both have to go through SlowShield, and they are set up
in different places: the base images in the container runtime, the dependencies in the Dockerfile.
1. Base images: the runtime
Configure the runtime once per machine, as on Container images:
- Docker on Linux:
/etc/docker/certs.d/_default/hosts.toml(the containerd image store, Docker's default since 29). It coversdocker buildas well asdocker pull. - Docker Desktop:
~/.docker/certs.d/_default/hosts.toml, then restart Docker Desktop. - Podman: the
registries.conf.ddrop-in, inside the machine on macOS and Windows.podman buildand Buildah use it too. - BuildKit builders of their own (
docker buildx create, BuildKit in CI):buildkitd.tomlwith mirrors. BuildKit goes to the registry itself after a refusal, so pair it with a network that only reaches SlowShield (step 3).
Alternatively, name SlowShield in the Dockerfile: FROM slowshield.example.com/docker.io/library/python:3.13-slim.
That works with any builder but ties the Dockerfile to your SlowShield.
2. Dependencies: the Dockerfile
A build container doesn't read your shell profile or your ~/.m2. Give each stage that installs
something an ARG named like the tool's setting. An ARG reaches RUN as an
environment variable and isn't kept in the image. With SlowShield as the default, every build uses it, and
--build-arg can still override it:
| Language | In the build stage |
|---|---|
| Python (pip) | ARG PIP_INDEX_URL=https://slowshield.example.com/pypi/simple/ |
| Python (uv) | ARG UV_DEFAULT_INDEX=https://slowshield.example.com/pypi/simple/ |
| JavaScript (npm, pnpm) | ARG npm_config_registry=https://slowshield.example.com/npm/ |
| Go | ARG GOPROXY=https://slowshield.example.com/go |
| Java (Maven) | a settings.xml with the mirror, written in a RUN step (Java) |
| Java (Gradle) | the init script in /root/.gradle/init.d/ (Java) |
| Rust | Cargo's config.toml, written in a RUN step (Rust) |
A complete example, a Go service on a distroless base:
FROM golang:1.25 AS build
ARG GOPROXY=https://slowshield.example.com/go
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app .
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
ENTRYPOINT ["/app"]Both FROM lines come through SlowShield via the runtime's configuration (step 1), the modules via the
ARG. An ARG applies to the stage it is declared in: repeat it in each stage that installs
something.
npm needs SlowShield's public name. SlowShield's tarball links are absolute, made from its public URL, so the build has to reach SlowShield under that name. With a real host name and HTTPS that's a given.
3. Check that nothing goes around it
Three checks, from quick to strict:
- Look at the dashboard. After a build, SlowShield's Packages page lists every base image and package it fetched. A dependency that doesn't show up came from somewhere else.
- Stop SlowShield and build again with
--no-cache --pull. The pull and every install step should fail. If something still downloads, it goes around SlowShield. - Build where SlowShield is the only thing reachable. A network without internet access that
contains SlowShield, and a builder on it. Anything that isn't configured fails, instead of quietly going to the
public registry. With Docker:
In CI, the same idea is an egress allowlist that only contains SlowShield.
# a network without internet access, joined by the container that serves # SlowShield's HTTPS (Caddy, in the Compose setup) docker network create --internal slowshield-only docker network connect --alias slowshield.example.com slowshield-only <caddy-container> # a builder on that network; buildkitd.toml from the Setup page docker buildx create --name slowshield-only --driver docker-container \ --driver-opt network=slowshield-only --buildkitd-config buildkitd.toml # RUN steps don't use the network's DNS: pass SlowShield's address docker buildx build --builder slowshield-only --load \ --add-host slowshield.example.com=$(docker inspect -f \ '{{(index .NetworkSettings.Networks "slowshield-only").IPAddress}}' <caddy-container>) .
What we tested
On 2026-10-07, on macOS with Docker Desktop (Docker 29.8.1, containerd image store) and Podman 6.1, against a SlowShield serving the real registries. Six small applications, each with dependencies from its registry:
| Application | Base images | Dependencies |
|---|---|---|
| Python, pip | python:3.13-slim | requests and 4 more from PyPI |
| Python, uv | python:3.13-slim, ghcr.io/astral-sh/uv | the same, with uv |
| JavaScript, npm | node:22-slim | express and 67 more from npm |
| Go | golang:1.25, gcr.io/distroless/static-debian12 | github.com/google/uuid, through the module proxy and the checksum database |
| Java, Maven | maven:3.9-eclipse-temurin-21, mcr.microsoft.com/openjdk/jdk:21-distroless | commons-lang3 and Maven's own plugins: 107 artifacts |
| Rust, Cargo | rust:1, gcr.io/distroless/cc-debian12 | itoa from crates.io |
- Docker, only SlowShield reachable (check 3): all six built and ran. Every base image and every dependency came through SlowShield; nothing else could be reached.
- Docker Desktop's own builder (
docker build, with thehosts.tomlin the VM): the five pip, npm, Go, Maven and Cargo applications built. SlowShield's download counts for each package doubled with the second round of builds. With SlowShield stopped,docker pullanddocker build --pullfailed instead of going to Docker Hub. - Podman (rootless machine, the drop-in): all six built through a second SlowShield, which served
the same 8 image repositories and the same packages. With it stopped,
podman pullandpip installfailed.
What we ran into, so you don't have to:
- BuildKit
RUNsteps don't resolve names on a custom Docker network (they get public resolvers). Pass--add-host, as in check 3. - Rootless Podman can't attach build steps to a named network ("cannot use networks as rootless"). For check 3 with Podman, use a rootful machine or a firewall instead.
- A SlowShield container on an internal Podman network with DNS can't resolve the registries: that
network's resolver answers first and only knows the network. Create the network with
--disable-dns, or give SlowShield--dns. - Plain HTTP (a local test instance) needs
PIP_TRUSTED_HOSTfor pip and Maven'smaven-default-http-blockeroverride; everything else accepted it. Use HTTPS for anything real.
For agents: this page as Markdown · llms.txt · the SlowShield skill