Advanced: how the container is configured
Each environment is a Docker container that Forge Fleet assembles from your
ffleet.toml and flags. This page explains the pieces so you can tune them.
Image
image (or --image) selects the Docker image the agent runs in. Default:
ghcr.io/grzegorz-aniol/forge-fleet-python:latest (pulled from GHCR on first
use — no local build required). A Go-dev variant
(ghcr.io/grzegorz-aniol/forge-fleet-go:latest, also published to GHCR) overlays
a Go toolchain for developing the Go port. Pick or build an image that carries the
language runtimes and tools your project needs — the agent can only use what's in
the image (plus what it installs at runtime).
Because image is a config-derived setting, you can change it and pick it up on
the next revive: ffleet stop SLUG then ffleet up SLUG (see
what can change mid-life).
How the published images are built
The two default images are built and pushed to GHCR from the public
ffleet-dist repo (its
publish-images workflow), where the Dockerfiles live:
ghcr.io/grzegorz-aniol/forge-fleet-python:latest— the base image (Debian bookworm-slim). It carries the agent toolbox: the Claude Code and Codex CLIs, Python 3 + uv, Node 22 (with corepack for pnpm/yarn), plusgit,gh(GitHub CLI), thedockerCLI (client only, for DooD),tmux,jq,make, and a C build toolchain.ghcr.io/grzegorz-aniol/forge-fleet-go:latest— a superset overlay of the base that adds the Go toolchain (for developing the Go port); the rest of the toolbox is identical.
Each is published multi-arch (linux/amd64 + linux/arm64), so Apple
Silicon Macs pull and run a native image without emulation.
Both run as a non-root user buddy (uid/gid 1000) with home
/home/buddy, and the workspace directory is owned by buddy. That is why credential and
cache mounts land under /home/buddy/... (e.g. ~/.claude →
/home/buddy/.claude), why extra_mounts into the container home use that
path, and why host Docker access adds buddy to the host docker group so it
can reach the socket (see DooD). If you build a custom image,
matching the buddy uid/gid 1000 keeps mounted-file ownership aligned with
your host user.
Volumes & mounts
Several things get mounted into the container:
- The workspace — the git worktree (or, in
--here/offmode, your current directory) is mounted into the container at its host-identical absolute path (e.g./srv/worktrees/creator-ai/creator-ai-feat1), not a fixed/workspace. This is where the agent does its work; it also gives every environment a unique container cwd, which matters for Codex session resume (see below). uv_cache_dir— a shared uv cache (default~/.cache/uv) for faster Python installs across environments.extra_mounts— your own bind mounts,source:target[:ro|:rw](the only accepted modes areroandrw, default read-write); target is an absolute container path, a relative source resolves against the repo root,~expands to the host home. Use these for shared caches, SSH keys, etc. See secrets for when to mount vs. copy vs. env-file.
Credential directories — authenticating the agent inside
The agent must be logged in inside the container. Forge Fleet handles this by mounting the agent's host credential/config directory:
- Claude Code —
[claude].dir(default~/.claude), plus a credentialauthsource (auto/keychain/token/api-key/credentials-file/external) that decides which credential ffleet injects. See the[claude].authtable. - Codex —
[codex].dir(default~/.codex).
Override the mounts per run with --claude-dir / --codex-dir, and the Claude
source with --claude-auth. The upshot: because your host login is mounted in,
you don't re-authenticate per environment.
Codex resumes the most recently used session within its cwd filter, not by ID. Unlike Claude, the Codex CLI has no way to resume a specific session by an externally chosen ID —
ffleet upon a stopped Codex environment runscodex resume --last, which (per Codex's own documentation for its--allflag) filters candidate sessions down to the ones started from the same working directory. Because each environment's container workspace is always mounted at its own host-identical path (see "Volumes & mounts" above), concurrent Codex environments get distinct cwds even though they share[codex].dirby default — so--lastresolves to the right environment's session in the common case. This isn't an absolute guarantee for every possible setup, so if you ever see a Codex environment resume the wrong session, giving that environment its own--codex-dirremoves the ambiguity entirely.
extra_hosts
extra_hosts maps hostnames to addresses via docker run --add-host, letting
processes inside resolve names to the host or to arbitrary addresses. Each entry
is host:address; the special value host-gateway resolves to the host itself —
handy for reaching a service running on your machine:
extra_hosts = ["host.docker.internal:host-gateway"]
Reaching a service on your host (e.g. an MCP server)
A common case: the agent needs to reach something running on your host — an MCP server, a local API, a dev database. Two pieces must line up:
- Give the container a name for the host. Map
host.docker.internalto the host gateway:
extra_hosts = ["host.docker.internal:host-gateway"]
Then point the agent at host.docker.internal:<port> (e.g. in your Claude MCP
config under the mounted ~/.claude).
2. Bind the host service so the container can reach it. host-gateway
reaches your host on its bridge interface, not its loopback — a service
bound only to 127.0.0.1 on the host is unreachable from the container. Bind
it to 0.0.0.0 (or the docker bridge address) instead.
Editing your host's /etc/hosts doesn't help here: it only changes name
resolution on the host, not inside the container (which has its own
/etc/hosts). The container-side mapping is exactly what extra_hosts provides.
Host Docker access
docker_host_bind = true (or --docker-host-bind) binds the host Docker socket
so the agent can drive the host daemon — this is the Docker-outside-Docker setup;
see DooD.
Git identity
Commits made inside the container use the [git] identity (user_name /
user_email) if both are set; otherwise identity falls back to your host git
config. Set both together or neither.
Putting it together
image = "ghcr.io/grzegorz-aniol/forge-fleet-python:latest"
uv_cache_dir = "~/.cache/uv"
extra_mounts = ["~/.ssh:/home/buddy/.ssh"]
extra_hosts = ["host.docker.internal:host-gateway"]
[claude]
dir = "~/.claude"
# auth = "auto"
[git]
user_name = "Jane Developer"
user_email = "jane@example.com"