← All articles

Building Docker development environments for autonomous coding agents

How to build a custom Docker image that gives your coding agent a complete, isolated development environment — with your app, its services, and everything it needs to run tests against real infrastructure.

10 min read

The setup bottleneck

Cloning a repository is easy. Getting it to actually run is where the time goes.

Most codebases need more than just the application code. A typical web app depends on a database, maybe a cache layer, sometimes a search index or a message queue. On a developer's host machine, setting all of this up means installing services, configuring ports, creating databases, setting environment variables, and hoping nothing conflicts with something else already running.

This is manageable — barely — when you're setting up one copy of the project. But when you're running multiple autonomous agents in parallel, each working on a different feature branch, the host-machine approach falls apart. Two agents trying to use the same Postgres instance. Port 3000 already taken. A test run that wipes a database another agent is using. These aren't hypothetical problems — they're the reason most teams can't scale agent workflows beyond a single instance.

One container, one environment, zero conflicts

The Trimo approach puts everything inside a single Docker container: your application, its services, and the coding agent itself. Each agent run gets its own container with its own filesystem, network, and process space. No shared state. No port conflicts. No "it works on my machine" surprises.

This means you can run five agents on five different feature branches, all using Postgres, all running tests — and none of them will ever interfere with each other.

The key is building a Docker image that matches your project's development environment. Not a production image with just the compiled app. A development image where the agent can install dependencies, run the dev server, execute tests, and interact with real services.


What goes into a development image

A development image for a coding agent needs three things:

  1. The language runtime — Node.js, Python, Go, Ruby, or whatever your project uses.
  2. Supporting services — databases, caches, queues, and any other infrastructure your app depends on.
  3. Development tools — package managers, build tools, test runners, and anything the agent needs to work with the codebase.

All of this lives in a single image, built on top of the Trimo base image. The base image provides the agent runtime, git safety wrappers, and the communication layer between the container and the Trimo daemon. Your custom image adds the project-specific environment on top.

Example: a Next.js app with Postgres and Redis

Consider a Next.js application that uses Postgres for its database and Redis for caching and session storage. On a developer's host machine, you'd need Node.js, a running Postgres server, and a running Redis server — plus the right database created, the right user configured, and the right ports available.

In a Docker image, you bundle all of that together:

FROM trimo/base:bookworm

# Install Node.js
RUN apt-get update \
    && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
    && apt-get install -y nodejs

# Install Postgres
RUN apt-get install -y postgresql postgresql-client

# Install Redis
RUN apt-get install -y redis-server

# Configure Postgres: create database and user
RUN service postgresql start \
    && su - postgres -c "psql -c \"CREATE USER app WITH PASSWORD 'app';\"" \
    && su - postgres -c "psql -c \"CREATE DATABASE app_dev OWNER app;\"" \
    && su - postgres -c "psql -c \"CREATE DATABASE app_test OWNER app;\"" \
    && service postgresql stop

# Set environment variables that survive the entrypoint's user switch
RUN echo 'export DATABASE_URL="postgresql://app:app@localhost:5432/app_dev"' \
    >> /etc/profile.d/trimo.sh

# Override the preboot script to launch services before the agent runs
COPY preboot.sh /opt/trimo/preboot.sh
RUN chmod +x /opt/trimo/preboot.sh

The preboot.sh script starts Postgres and Redis before the agent begins working:

#!/bin/bash
set -euo pipefail
service postgresql start
redis-server --daemonize yes

When Trimo starts a container from this image, it clones the repo, checks out the pipeline's branch, runs the preboot script (which starts Postgres and Redis), and then hands control to the coding agent. The agent can npm install, run migrations, start the dev server, and execute tests — all against real services, all inside the container.


Why real services instead of mocks

You could mock every external dependency. Many test setups do. But mocks drift from reality. A mock database doesn't enforce foreign key constraints. A mock cache doesn't expose timing behaviour. A mock queue doesn't reveal message ordering issues.

When an autonomous agent runs tests against mocks, the tests might pass — but the code might still break in production. The agent can't catch what the mocks don't simulate. And because the agent is working autonomously, there's no developer watching who might think "that mock looks suspicious."

Real services inside the container give the agent the same feedback loop a developer would have on their local machine. If a migration is broken, the real database throws an error. If a query is slow, it's slow against real data. If a Redis key isn't being set correctly, the tests fail for the right reason.

This is especially important for autonomous agents that run without supervision. The quality of the feedback they get determines the quality of the code they produce. Real services produce real feedback.


The isolation advantage

On a developer's host machine, every project shares the same system services. Postgres on port 5432. Redis on port 6379. One database server for everything.

This creates subtle, hard-to-diagnose problems:

  • Shared databases. Two projects using the same Postgres instance. A test run in one project drops tables that another project depends on. You don't notice until hours later when a different test suite fails for no apparent reason.
  • Port collisions. You clone a fork to investigate a bug. You run npm run dev, and it fails because port 3000 is already in use by the other copy. You change the port, forget about it, and now your environment variables are wrong for the next session.
  • Version conflicts. One project needs Postgres 15, another needs Postgres 17. One needs Node 20, another needs Node 22. You install version managers, juggle configurations, and occasionally break things when you switch.
  • Leftover state. A database migration creates a table. You switch branches. The table is still there. Your tests pass because the table exists, but they'd fail on a clean database. You ship, it breaks in CI.

With Docker containers, each agent gets a completely fresh environment. There is no shared state. There are no lingering tables from a previous branch. There are no port conflicts because each container has its own network namespace. Every run starts clean.

This is the same principle behind CI pipelines running each build in a fresh environment. The difference is that with Trimo, you get this isolation locally, on your own machine, for every agent run — not just on push to a remote branch.


More examples

A Python Django app with Postgres and Celery

FROM trimo/base:bookworm

# Python and pip
RUN apt-get update && apt-get install -y python3 python3-pip python3-venv

# Postgres
RUN apt-get install -y postgresql postgresql-client
RUN service postgresql start \
    && su - postgres -c "psql -c \"CREATE USER django WITH PASSWORD 'django';\"" \
    && su - postgres -c "psql -c \"CREATE DATABASE django_dev OWNER django;\"" \
    && service postgresql stop

# Redis (Celery broker)
RUN apt-get install -y redis-server

# Preboot script
COPY preboot.sh /opt/trimo/preboot.sh
RUN chmod +x /opt/trimo/preboot.sh

The agent can run python manage.py migrate, python manage.py test, and even start Celery workers — all inside the container, all against real Postgres and Redis.

A Go API with Postgres and Elasticsearch

FROM trimo/base:bookworm

# Go — install and persist PATH for the agent user
RUN curl -fsSL https://go.dev/dl/go1.23.0.linux-amd64.tar.gz \
    | tar -C /usr/local -xzf -
RUN echo 'export PATH="/usr/local/go/bin:$${PATH}"' >> /etc/profile.d/trimo.sh

# Postgres
RUN apt-get update && apt-get install -y postgresql postgresql-client
RUN service postgresql start \
    && su - postgres -c "psql -c \"CREATE USER goapp WITH PASSWORD 'goapp';\"" \
    && su - postgres -c "psql -c \"CREATE DATABASE goapp_dev OWNER goapp;\"" \
    && service postgresql stop

# Elasticsearch
RUN apt-get install -y default-jdk wget \
    && wget -qO- https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.15.0-linux-x86_64.tar.gz \
    | tar -C /opt -xzf -
RUN echo 'export ES_HOME="/opt/elasticsearch-8.15.0"' >> /etc/profile.d/trimo.sh

COPY preboot.sh /opt/trimo/preboot.sh
RUN chmod +x /opt/trimo/preboot.sh

The pattern is the same: install the runtime, install the services, configure them, and start them before the agent runs.


Design principles for agent development images

Keep it self-contained

Everything the agent needs should be inside the image. Don't rely on network access to external services. Don't mount host directories for databases. The container should be able to clone the repo, install dependencies, and run the full test suite without reaching outside its own boundary.

Start services in preboot

Trimo runs a preboot script before handing control to the agent. Override /opt/trimo/preboot.sh to start Postgres, Redis, Elasticsearch, or whatever your app needs. By the time the agent begins, all services should be running and ready to accept connections.

Use sensible defaults

Don't make the agent figure out connection strings. Set environment variables with default credentials and connection URLs. The agent should be able to run npm test or go test ./... without any additional configuration. Note: Trimo's entrypoint switches to a non-root user with a login shell, so use /etc/profile.d/trimo.sh for environment variables instead of Dockerfile ENV directives — otherwise they won't be available to the agent.

Create both dev and test databases

If your test framework expects a separate test database, create it in the image. The agent shouldn't need to run database setup commands manually — that's the kind of step that gets missed and wastes an entire agent run debugging "relation does not exist" errors.

Optimise for rebuild speed

You'll iterate on your development image as your project evolves. Structure your Dockerfile so that the layers that change least (language runtime, service installation) come first, and project-specific configuration comes last. This keeps rebuilds fast.


From hours of setup to 15 minutes

Building a custom development image is a one-time investment. Once the image exists, every agent run starts with a fully configured environment. No manual setup. No debugging port conflicts. No "did you remember to create the test database?" messages in Slack.

For a typical project, expect the initial image build to take a few hours — mostly spent figuring out which services you need and how to configure them. After that, the image rebuilds in minutes when you need to update a dependency or add a new service.

Compare this to the alternative: every time you want to run an agent on a new branch, you'd need to ensure the host environment is configured correctly, that no other agent is using the same ports or databases, and that leftover state from a previous run isn't going to cause false positives in tests. That's not a one-time cost — it's a tax on every run.

With Trimo, you install the CLI, build your image, and start dispatching agents. Each agent gets a clean, isolated environment with real services. No conflicts. No shared state. No surprises.


Related articles