# Docker and builds

> How Egress CI holds the job's containers, docker compose, docker build and image pulls to the same allowlist as the job.

With `containers: track` (the default) and root reachable through `sudo` or Docker, the job's Docker goes through a socket the sidecar serves. The action sets `DOCKER_HOST` to it, so `docker`, `docker compose`, Testcontainers and any other client that reads `DOCKER_HOST` use it without changes.

## What the job's socket does

- **Labels.** Containers, networks and volumes created through it get the label `org.openwaf.egress-ci.uid` with the job user's id.
- **A network for the job.** The sidecar creates a bridge network for the job. A container that asks for Docker's default bridge gets that network instead; a container that names its own network (Compose does) keeps it.
- **Covered from the first packet.** Each network created through the socket enters the lock by subnet as soon as it exists, before any container joins it. A container's DNS goes to the sidecar's forwarder and its TCP to the transparent proxy, exactly like the job's own; the rest is refused in enforce. Traffic between the job's containers on those networks is not restricted.
- **Attributed.** The summary's **Via** column says `container` and names the container.
- **Daemon unchanged.** Every call is forwarded to the daemon the runner gave the job (its `DOCKER_HOST`, or `/var/run/docker.sock`).

## docker build

The action creates a job builder (BuildKit in a container, `docker-container` driver) through the job's socket and sets `BUILDX_BUILDER` to it. `docker build` and `docker buildx build` use it and load the result into the daemon, so `docker run` of the built image works as before.

Because the builder is one of the job's containers, every `RUN` step is held to the policy: a `RUN curl` to a name outside it is refused in enforce. The daemon's own builder runs build steps in sandboxes no job can be told apart in, which is why the action does not use it.

The builder's BuildKit state (cache of base images and layers) is kept for the next job of the same runner user, pruned to 4 GB. On GitHub-hosted runners each job gets a new machine, so it starts empty every time.

The builder's image, `moby/buildkit`, comes from Docker Hub on the first build: Docker Hub must be allowed (below).

## Image pulls

A pull through the job's socket (`docker pull`, `docker run` of a missing image, Compose) is checked by registry before the daemon fetches anything. `ghcr.io/acme/app:1` is checked against `ghcr.io`; a Docker Hub image (`alpine:3`, `docker.io/library/alpine`) against `registry-1.docker.io`. A registry outside `allow` is refused in enforce with `403` and the reason `registry_not_allowed`. The summary lists every pull with its decision.

`docker pull` and `docker run` fetch layers through the daemon, whose own connections are not proxied: for them only the registry check applies. The job builder pulls base images itself, from inside the lock, so its downloads must be allowed too. For Docker Hub, allow:

```yaml
allow:
  - auth.docker.io                     # tokens
  - registry-1.docker.io               # registry (every docker.io pull is checked against this name)
  - production.cloudflare.docker.com   # layers
  - production.cloudfront.docker.com
```

## Example

```yaml
jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
      - uses: openwaf-dev/egress-action@v0.3.2
        with:
          containers: lock-all
      - run: docker build -t app:ci .
      - run: docker compose up --abort-on-container-exit --exit-code-from tests
```

## containers: track, lock-all or off

| Value | Use it when |
|---|---|
| `track` | Default. The job's own containers follow its policy. Containers of other jobs on the same host are only observed (listed under «flows from containers that are not this job's»). |
| `lock-all` | The runner runs one job at a time: GitHub-hosted runners, ephemeral self-hosted runners. In enforce it also refuses every other container's traffic outside the policy, which covers Docker used without `DOCKER_HOST`. Never on a host that runs several jobs at once: it would refuse the other jobs' containers. |
| `off` | Docker is left untouched: no socket, no builder, no container coverage. |

Docker tracking needs root (the lock), the daemon's socket and the `docker0` bridge. Without them the action runs with Docker untouched and the summary says `Docker off`; in enforce, a socket that fails to start fails the step.

## What is not covered

- A tool that opens `/var/run/docker.sock` itself instead of reading `DOCKER_HOST`: its containers are not attributed; with `lock-all` their traffic outside the policy is still refused.
- Containers started with `--network host` are outside the lock unless they run as the job's user.
- The daemon's own connections (pushes, and the downloads of a pull once its registry is allowed) are not proxied.
- Destinations in Docker's address pool (`172.16.0.0/12`) are left to the runner's rules.
