How to run multiple AI coding agents in Docker on a remote machine
One AI agent on one task is useful. Four agents on four tasks — while you review the first pull request — is where a remote machine pays off. The trick is keeping them out of each other's way and out of anything they shouldn't touch. Here is how I run several agents side by side with Docker.
The idea in one picture
One remote Linux machine, reachable only over a private VPN (see Android builds on a Linux VPS over WireGuard). On it, every agent gets:
- its own container — its own processes, limits and filesystem;
- its own git clone on its own branch — no two agents edit the same working copy;
- its own caches — no lock fights over Gradle or npm caches;
- one task — written down before it starts.
Everything they produce comes back as pull requests, and CI decides what is good enough to merge.
Step 1 — one image for all agents
Build one image with your toolchain and a non-root user whose ID matches the owner of the clones on the host, so files stay writable on both sides:
FROM eclipse-temurin:17-jdk
RUN apt-get update \
&& apt-get install -y --no-install-recommends git curl unzip ruby-full build-essential tmux \
&& rm -rf /var/lib/apt/lists/*
ARG UID=1000
RUN userdel -r ubuntu 2>/dev/null || true \
&& useradd -m -u $UID agent
USER agent
WORKDIR /work
Install your agent CLI of choice in the same image (the steps differ per tool). The Ubuntu-based base image ships a default ubuntu user with ID 1000, which is why it is removed first.
Step 2 — one clone per agent
mkdir -p ~/agents/work
for n in 1 2 3; do
git clone git@github.com:acme/app.git ~/agents/work/agent-$n
git -C ~/agents/work/agent-$n switch -c agent-$n/task
done
Why not git worktree? A worktree's .git file points to the main repository by its absolute path on the host. Mount only the worktree into a container and git inside it can't find that path. Separate clones cost a little disk and avoid the problem entirely.
Step 3 — Docker Compose with a shared agent template
x-agent: &agent
build:
context: .
args:
UID: "1000"
image: agent-box
init: true
stdin_open: true
tty: true
command: sleep infinity
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
mem_limit: 6g
cpus: 2
pids_limit: 512
env_file: agent.env # model API key + scoped git token, nothing else
services:
agent-1:
<<: *agent
volumes:
- ./work/agent-1:/work
- gradle-1:/home/agent/.gradle
agent-2:
<<: *agent
volumes:
- ./work/agent-2:/work
- gradle-2:/home/agent/.gradle
agent-3:
<<: *agent
volumes:
- ./work/agent-3:/work
- gradle-3:/home/agent/.gradle
volumes:
gradle-1: {}
gradle-2: {}
gradle-3: {}
docker compose up -d --build
Limits matter more than they look: without mem_limit, one agent's runaway Gradle build can push the whole machine into swap and slow every other agent to a crawl.
Step 4 — what the containers must never get
- The Docker socket. Mounting
/var/run/docker.sockinto a container gives it control of the host — effectively root. Agents never get it. - Release secrets. No signing keys, store credentials or production database URLs. The env file holds a model API key and a git token that can push branches and open pull requests — nothing else. Releases stay in CI, behind a human-approved environment.
- Your SSH keys and shell history. Only the agent's own clone is mounted.
Step 5 — work with the agents
Start each agent in a tmux session on the host, so sessions survive when you disconnect from the VPN:
tmux new -d -s agent-1 'docker compose exec agent-1 bash'
tmux attach -t agent-1
Check how hard the machine is working at any time:
docker stats --no-stream
To preview an agent's dev server, publish its port on the VPN address only — never on all interfaces:
agent-1:
<<: *agent
ports:
- "10.8.0.1:3001:3000"
Agent 1's app is then at http://10.8.0.1:3001 for devices on the VPN, and invisible from the internet.
Step 6 — coordinate the work
- Split tasks by module, not by step. “Agent 1: login screen. Agent 2: crash in sync. Agent 3: tests for billing” — not three agents on one feature.
- Write each task down in the agent's branch (a short
TASK.mdworks) together with the repository's agent instructions file, so every agent starts from the same rules. - One branch, one pull request per agent. CI runs the same checks for every PR; conflicts are solved at review time, not by agents overwriting each other.
More about those rules in what an AI agent harness is.
Step 7 — reset an agent
When a task is merged — or goes wrong — throw the state away instead of repairing it:
docker compose rm -sf agent-2
git -C ~/agents/work/agent-2 fetch origin
git -C ~/agents/work/agent-2 switch -C agent-2/next-task origin/main
docker compose up -d agent-2
The container starts clean, the clone is back on the latest main, and the agent's build cache volume keeps builds fast.
Checklist
- Machine reachable only over the VPN; dev servers bound to the VPN address.
- One container, one clone, one branch and one task per agent.
- Non-root user, capabilities dropped, memory, CPU and process limits set.
- No Docker socket, no release secrets, no personal keys in containers.
- Separate cache volumes per agent.
- Agents deliver pull requests; CI and a human decide what merges.