← Blog
Infrastructure · 9 min read

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.sock into 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.md works) 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.
Few spots left Free AI audit