Skip to content

Docker Compose

Compose is the only deployment path this repository ships ready to use. There are two files: docker-compose.yml (development, against a control-plane of your own) and docker-compose.fleet.yml (an overlay for production).

Terminal window
npm run dev:runner # = infisical run … -- docker compose up --build
docker compose up --build # the same thing, without Infisical

The dev:runner script wraps Compose in Infisical because that’s the vault this team uses. If you don’t use Infisical, the second command works identically: credentials just need to arrive through one of the two paths below.

Compose uses a list under environment:, not a map, precisely so it can mix fixed values (NAME=value) with pass-through (bare NAME, no =). Pass-through means: take it from the host shell, and if it doesn’t exist on the host, the key is omitted in the container, and it does not become an empty string.

There are two paths, in order of preference:

Terminal window
# 1. vault → shell → pass-through
RUNNER_CONNECTORS=real DD_API_KEY= GITHUB_TOKEN= npm run dev:runner
Terminal window
# 2. file (offline fallback)
cp runner.secrets.env.example runner.secrets.env
# fill it in, and the env_file picks it up
docker compose up --build

With neither, the runner stays in synthetic: fake data, no credentials.

volumes:
- ./dev-certs:/certs:ro
- ${AWS_CONFIG_DIR:-${HOME}/.aws}:/root/.aws:ro

dev-certs/: the development PEMs minted by npm run dev:certs. The docker-entrypoint.sh reads them from /certs and exports them as RUNNER_CERT_PEM, RUNNER_KEY_PEM, and RUNNER_CA_PEM, because the runner reads PEM inline, not by path, which avoids the nightmare of multiline PEMs in an env_file.

~/.aws: the required companion to RUNNER_AWS_CRED_MODE=chain. Without this volume the container can’t see the cache aws sso login writes, and the credential chain fails without saying so, the classic “works in my terminal, not in the container”. When the SSO token expires, run aws sso login on the host: the container picks up the refreshed cache with nothing to restart.

extra_hosts:
- "host.docker.internal:host-gateway"

On Linux this maps host.docker.internal to the host; on Docker Desktop it already resolves and the line is a no-op. It’s what lets RUNNER_TUNNEL_URL=wss://host.docker.internal:8443 reach a control-plane running on your machine.

In development the runner picks itself back up. That’s intentional, and it pairs with the 1006 → exited 0 → restart cycle you see when the tunnel on the other side restarts: the exited 0 is the reconnect timer being unref’d, and the restart brings it back when the tunnel returns.

In production this behavior is inverted on purpose. See Production deployment.

Why the control-plane isn’t in this Compose file

Section titled “Why the control-plane isn’t in this Compose file”

It consumes the @rootpilot/* packages through a file link to the sibling edge repository, which sits outside any Docker build context. Containerizing it is blocked by a project interim. The development arrangement is: control-plane on the host, runner in the container.