A Debian-based container for running Claude Code / opencode isolated from the host, attached to with herdr over SSH. Runs locally for now; moving it to a remote host later is a matter of changing the SSH target, not the image.
Ships: claude, opencode, copilot, codex, neovim (latest release), gh, glab, uv, chezmoi
(applies this dotfiles repo on build), beads
(bd, an AI-supervised issue tracker), and sshd so herdr can attach to a
persistent session inside the container.
Authorize an existing key to log in as agent. If your keys live in an SSH
agent (1Password's SSH agent, ssh-agent, etc.) rather than as files under
~/.ssh, get the public key from the agent instead of cat-ing a file that
may not exist:
ssh-add -LPick the line for the key you want to use and write it to authorized_keys
(a dedicated key is best, if you have one — this never needs your general
personal key):
echo 'ssh-ed25519 AAAA... key-comment' > authorized_keys(gitignored via the repo's root .gitignore and kept out of the image via
.dockerignore regardless.)
The Host agent-container entry that herdr (and plain ssh) needs lives in
ssh_config next to this README — this is the file to edit when you move to
a remote host later (just change HostName). Pull it into your real SSH
config with an Include, added near the top of ~/.ssh/config since Include
is processed in place and ssh_config uses first-match-wins:
# ~/.ssh/config
Include ~/.config/kris-steinhoff/agent-container/ssh_config
ForwardAgent yes in that entry lets git inside the container use your
host's ssh-agent for clone/push over SSH, without ever putting a private key
in the image.
Corporate proxies that re-sign HTTPS with a private root CA break every
curl/apt/npm/git fetch in the build (and claude/opencode at
runtime), since the container doesn't trust that root. Drop the root (or
bundle) into certs/ as a *.crt (PEM) file and the build adds it to the
container trust store:
cp /path/to/corp-root-ca.pem certs/corp.crtcerts/ ships empty, so this is a no-op off-network. Real certs are
gitignored and stay out of the repo.
docker compose build
docker compose up -d
herdr --remote agent-containerclaude, copilot, codex, herdr, and bd sit below a cache gate at the
bottom of the Dockerfile so they can reinstall without busting the expensive
base layers (apt, neovim, chezmoi plugin pre-fetch). The gate is the TOOLS_REFRESH
build arg: change its value and only those tools rebuild. A plain build defaults
it to 0, which reuses the cache. To always pull the latest of those tools,
alias the build command to pass a fresh timestamp:
alias agent-up='TOOLS_REFRESH=$(date +%s) docker compose up -d --build'Everything else (including opencode, neovim, terraform, gh, glab)
stays cached until you edit the Dockerfile or build with --no-cache.
herdr --remote installs herdr on the container the first time it connects
and gives you a persistent session — detach and reattach freely, and it
survives your local terminal closing.
Not baked into the image. Either:
- Run
claude/opencodeinside the container once and complete the normal interactive login, which persists in theagent_homevolume, or - Set
ANTHROPIC_API_KEY/OPENCODE_API_KEYin the shell beforedocker compose up(compose reads them from the environment; seedocker-compose.yml).
gh needs its own login too — gh auth login inside the container (persists
in agent_home), separate from the ForwardAgent-based git-over-SSH access.
glab is the same story — glab auth login for GitLab access.
agent_home is a named volume mounted at /home/agent — it's seeded from
the image on first start (dotfiles, herdr, nvim plugins already installed)
and then persists auth tokens, shell history, and anything else you don't
mount separately across docker compose restart/down+up. docker compose down -v deletes it.
ssh_host_keys persists the sshd host keys across rebuilds so your local
known_hosts doesn't need updating every time you rebuild the image.
To work on a project, bind-mount it in — see the commented example in
docker-compose.yml. Anything not mounted only exists in agent_home.
In LM Studio, Developer tab → Server Settings → enable "Serve on Local
Network" (it binds 127.0.0.1 by default, which the container can't reach
at all — note LM Studio's server has no auth by default, so this exposes it
to your whole LAN, not just the container). Note the port (default 1234).
docker-compose.yml already maps host.docker.internal to the Mac through
Colima's VM (extra_hosts: host-gateway).
LM Studio 0.4.1+ serves a native Anthropic-compatible /v1/messages
endpoint, so claude can point straight at it — no proxy, no opencode
detour:
export ANTHROPIC_BASE_URL=http://host.docker.internal:1234
export ANTHROPIC_AUTH_TOKEN=lmstudioopencode works the same way via its OpenAI-compatible endpoint if you'd
rather use that instead; see its provider config docs.
Rebuild the image on (or push it to) the remote host, run the compose stack
there, then just point the Host agent-container block in ~/.ssh/config
at the remote address instead of localhost. Nothing about the container
or the herdr invocation changes.
zsh-autosuggestions/zsh-syntax-highlightingin the shared zshrc are only sourced when Homebrew is present, which this container doesn't have (by choice, to keep the image apt-only). The shell works, just without those two plugins.