# 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](https://slowshield.org/docs/containers/):

- **Docker on Linux:** `/etc/docker/certs.d/_default/hosts.toml` (the containerd image store, Docker's default since 29). It covers `docker build` as well as `docker pull`.

- **Docker Desktop:** `~/.docker/certs.d/_default/hosts.toml`, then restart Docker Desktop.

- **Podman:** the `registries.conf.d` drop-in, inside the machine on macOS and Windows. `podman build` and Buildah use it too.

- **BuildKit builders of their own** (`docker buildx create`, BuildKit in CI): `buildkitd.toml` with 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](https://slowshield.org/docs/java/#docker)) |
| Java (Gradle) | the init script in `/root/.gradle/init.d/` ([Java](https://slowshield.org/docs/java/#gradle)) |
| Rust | Cargo's `config.toml`, written in a `RUN` step ([Rust](https://slowshield.org/docs/rust/#docker)) |

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:

1. **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.

2. **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.

3. **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:
   ```
   # 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>) .
   ```
   In CI, the same idea is an egress allowlist that only contains SlowShield.

## 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 the `hosts.toml` in 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 pull` and `docker build --pull` failed 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 pull` and `pip install` failed.

What we ran into, so you don't have to:

- **BuildKit `RUN` steps 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_HOST` for pip and Maven's `maven-default-http-blocker` override; everything else accepted it. Use HTTPS for anything real.

---

This page as HTML: https://slowshield.org/docs/container-builds/. All of the guide in one file: https://slowshield.org/llms-full.txt
