Build a Dockerfile that includes your standard tools (Claude Code / Codex CLI, Python deps, internal CLI, your CLAUDE.md, git config, ssh known_hosts). Push to ECR. Reference the image when creating sandboxes. Cold-start drops from ~120 sec to ~30 sec; warm-pool size requirements drop accordingly. At 1,000 daily runs, saves ~$8/day in cold-start tax. Use immutable date-based tags (v2026.05.10-001), never :latest in production. Multi-stage builds keep image under 2 GB.
Why custom images matter for AI agent fleets
Three reasons custom images are the right pattern for production AI agent fleets:
- Cold start is dead time. A typical agent run is 30 sec - 5 min. If 90 sec of that is "install Claude Code, pip install dependencies, pull docker images" — every run has a 30-50% overhead tax. At 1,000 runs/day, that's $8/day in compute spent on setup, not work.
- Reproducibility. Production agents need known-good environments. Pinned dependency versions, validated tool versions, tested integrations. Custom images make this an artifact you can reason about — a specific image tag is a specific environment.
- Audit and supply chain. Compliance regimes increasingly want to know "exactly what software was on the box when the agent ran?" Custom images with signed manifests answer that. Default base images leave that question open.
Operators who don't bake images live with the cold-start tax + the reproducibility flake + a fuzzy supply-chain story. Operators who do bake spend two engineering weeks on the pipeline, then never think about it again. The math is dramatic.
When to bake your own image (and when not to)
Custom images aren't free — there's a CI/CD pipeline to build, an image registry to manage, a versioning discipline to follow, and a CVE-scanning cadence to maintain. The cost is small but real. Use this decision tree:
Bake if:
- Every agent run installs the same set of dependencies (you're paying the install cost N times)
- You have an internal CLI / tool that the agent uses
- You need specific git / ssh / cred configuration
- You want a known-good "blessed" environment, version-controlled separately from agent runs
- Compliance / audit needs to verify the agent's runtime environment
- You run more than ~50 agent invocations per day (cold-start tax is meaningful)
- You've hit a flaky-install issue (PyPI down, npm registry slow, GitHub rate-limited) more than once
Don't bake if:
- You only run a handful of times a week (the image-maintenance overhead exceeds the cold-start savings)
- Each run needs different dependencies (use the install-on-demand path)
- Your "tools" are stable and the platform's default image already has them (Claude Code, Codex CLI, jq, ripgrep, fd are pre-installed)
- You're prototyping — wait until the agent is past shakedown before baking
- The image would be heavier than 5 GB (pull time dominates; consider runtime mounts instead)
Decision-table by use case
| Use case | Bake custom image? | Why |
|---|---|---|
| AP clerk processing 50 invoices/day | Yes | 50 daily runs × 90 sec install = 75 min/day wasted |
| Paralegal doing 10 NDA reviews/day | Marginal — yes if internal tools | 10 runs × 60 sec = small saving; bake if you have firm-specific tools |
| Always-on monitoring agent | Yes | Started rarely, but reproducibility matters; CVE scanning matters |
| One-off research investigation | No | Single invocation; the install cost is one-time |
| Customer-support tier-1 (high volume) | Yes | Hundreds of daily runs; cold-start tax compounds |
| Custom Docker image you'd ship to customers | Yes — required | Known artifact for SaaS multi-tenant |
| Development / prototyping agent | No | Iteration speed matters more than startup time |
| GPU ML inference agent | Yes | CUDA + drivers must be pinned and tested |
Reference Dockerfile #1: Coding agent (Claude Code / Codex CLI)
# Start from Sandbox Platform's blessed base image FROM registry.ab0t.com/agent-base:1.0 # Stage 1: install agent harness + system tools RUN apt-get update && apt-get install -y --no-install-recommends \ git ripgrep fd-find jq curl gnupg \ build-essential pkg-config \ && rm -rf /var/lib/apt/lists/* # Install Claude Code (pin a specific version for reproducibility) RUN npm install -g @anthropic-ai/claude-code@1.7.3 # Install Codex CLI as alternative harness RUN pip install --no-cache-dir openai-codex-cli==2.4.1 # Stage 2: install Python deps the team commonly uses COPY requirements.txt /tmp/ RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt # Stage 3: install team's internal CLI from S3 or GHCR RUN curl -sS https://internal.example.com/cli/install.sh | bash # Stage 4: configure git defaults (the agent will commit; this is its identity) RUN git config --global user.email "agent@yourcompany.com" \ && git config --global user.name "AI Agent (CompanyName)" \ && git config --global init.defaultBranch main # Stage 5: SSH known-hosts for your internal git server COPY known_hosts /home/ec2-user/.ssh/known_hosts RUN chown ec2-user:ec2-user /home/ec2-user/.ssh/known_hosts \ && chmod 644 /home/ec2-user/.ssh/known_hosts # Stage 6: standard agent job description (mountable, but baked default exists) COPY CLAUDE.md /etc/claude.md ENV CLAUDE_GLOBAL_CONFIG=/etc/claude.md # Default to ec2-user (don't run as root) USER ec2-user WORKDIR /workspace # Healthcheck for systemd HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD claude --version || exit 1 # Default command — agent harness reads from CLAUDE_GLOBAL_CONFIG CMD ["claude", "--resume"]
Build size: ~1.2 GB (base 700 MB + Claude Code + Python deps + internal CLI). Build time: ~3-5 min on a CI runner with caching.
Reference Dockerfile #2: Browser agent (Anthropic Computer Use / Operator)
# Start from a desktop-capable base for computer-use agents FROM registry.ab0t.com/xfce-base:1.0 # Browser binaries RUN apt-get update && apt-get install -y --no-install-recommends \ chromium-browser firefox-esr \ x11-utils xauth dbus-x11 \ libnss3 libxcomposite1 libxdamage1 libxrandr2 \ libgbm1 libxkbcommon0 libpango-1.0-0 \ && rm -rf /var/lib/apt/lists/* # Playwright for programmatic browser control RUN pip install --no-cache-dir playwright==1.42.0 \ && playwright install chromium firefox \ && playwright install-deps # noVNC for operator-side viewing of the agent's screen RUN apt-get update && apt-get install -y --no-install-recommends \ novnc websockify x11vnc xvfb \ && rm -rf /var/lib/apt/lists/* # Anthropic computer-use SDK RUN pip install --no-cache-dir anthropic[computer-use]==0.45.0 # Pre-warm browser cache (saves first-launch time) RUN chromium-browser --headless --disable-gpu --dump-dom about:blank > /dev/null # Standard CLAUDE.md for browser agents (mountable but baked default) COPY CLAUDE.md /etc/claude.md USER ec2-user WORKDIR /workspace # Browser agents launch via a startup script that brings up Xvfb + browser + agent CMD ["/usr/local/bin/start-browser-agent.sh"]
Build size: ~2.4 GB (XFCE base + browsers + Playwright). Larger than coding agents because the GUI stack is substantial. Worth it for browser workloads — the alternative is 60+ sec at every cold start to install the same stack.
Reference Dockerfile #3: Vertical agent (legal / finance / healthcare)
# Multi-stage: build context separately from runtime image FROM python:3.12-slim AS builder RUN apt-get update && apt-get install -y build-essential COPY requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt # Runtime stage — slimmer FROM registry.ab0t.com/agent-base:1.0 # Copy installed packages from builder COPY --from=builder /root/.local /home/ec2-user/.local # Vertical-specific tools RUN npm install -g @anthropic-ai/claude-code@1.7.3 # Citation-verifier tool (this firm's specific compliance requirement) COPY citation-verifier /usr/local/bin/citation-verifier RUN chmod +x /usr/local/bin/citation-verifier # NetDocuments / iManage CLI (firm uses this) RUN pip install --no-cache-dir \ netdocuments-cli==3.1.0 \ imanage-cli==2.5.0 # Bluebook citation linter (must-have for legal work) RUN pip install --no-cache-dir bluebook-linter==1.2.0 # Westlaw / Lexis API clients RUN pip install --no-cache-dir westlaw-api==2.0.3 lexis-api==1.4.1 # Standard CLAUDE.md template for paralegal role COPY claude-paralegal.md /etc/claude.md # Environment ENV PATH="/home/ec2-user/.local/bin:$PATH" USER ec2-user WORKDIR /workspace CMD ["claude", "--resume"]
Build size: ~1.6 GB. Vertical-specific tools layered on top of the base. Update cycle for these images is typically tied to the firm's tool refreshes (quarterly is fine; weekly is overkill for stable vertical tooling).
CI/CD: GitHub Actions workflow
You don't build images by hand in production. CI builds, scans, and pushes:
name: Build Agent Image on: push: branches: [main] paths: - 'docker/agent/**' - '.github/workflows/build-agent-image.yml' schedule: # Weekly rebuild for security patches even if no code change - cron: '0 6 * * 1' workflow_dispatch: jobs: build: runs-on: ubuntu-22.04 permissions: id-token: write # for OIDC to AWS contents: read steps: - uses: actions/checkout@v4 - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr-push aws-region: us-west-2 - name: Login to ECR id: ecr-login uses: aws-actions/amazon-ecr-login@v2 - name: Compute version tag id: version run: | DATE=$(date +%Y.%m.%d) BUILD_NUM=$(date +%H%M) echo "tag=v${DATE}-${BUILD_NUM}" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build & push uses: docker/build-push-action@v5 with: context: ./docker/agent push: true tags: | ${{ steps.ecr-login.outputs.registry }}/agent:${{ steps.version.outputs.tag }} ${{ steps.ecr-login.outputs.registry }}/agent:latest-staging cache-from: type=gha cache-to: type=gha,mode=max - name: Scan image for CVEs (Trivy) uses: aquasecurity/trivy-action@master with: image-ref: ${{ steps.ecr-login.outputs.registry }}/agent:${{ steps.version.outputs.tag }} format: 'sarif' output: 'trivy-results.sarif' severity: CRITICAL,HIGH - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' - name: Test the image (smoke) run: | docker run --rm ${{ steps.ecr-login.outputs.registry }}/agent:${{ steps.version.outputs.tag }} \ claude --version - name: Notify Slack if: success() run: | curl -X POST $SLACK_WEBHOOK -d "{\"text\": \"Agent image v${{ steps.version.outputs.tag }} pushed\"}" env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
What this does:
- OIDC to AWS — no long-lived credentials in GitHub Secrets. The CI assumes a role via OIDC; the role has scoped permissions.
- Date-based versioning —
v2026.05.10-1430is unambiguous and chronologically sortable. - Buildx with cache — gha cache backend means subsequent builds reuse layers.
- Two tags — the immutable date tag for production references;
latest-stagingfor staging deployments. - Trivy scan — CVE check on every build; results in GitHub Security tab.
- Smoke test — verifies the image at minimum runs
claude --versionsuccessfully. - Slack notification — engineering channel sees every build.
- Weekly cron — even if no code change, rebuild weekly to pick up base-image security patches.
Versioning: never :latest in production
Use immutable tags. Three reasons:
- Reproducibility. If someone investigates a 6-month-old run, they need the same image to reproduce.
:latestat that time and:latestnow are different images; you've lost the ability to reproduce. - Rollback. Bad image push doesn't take down the fleet — you reference the previous tag. With
:latest, a bad push is global. - Audit. The audit log shows which image version each run used.
:latestmakes that meaningless.
Three tag schemes that work
| Scheme | Example | Best for |
|---|---|---|
| Date-based | v2026.05.10-1430 | Continuously-rebuilt images; "image of the day" semantics |
| Semver | v1.4.7 | Stable APIs; rare rebuilds; explicit breaking-change signaling |
| Git SHA | sha-a1b2c3d | Tied to repo; useful for "exact source" debugging |
| Combination | v1.4.7-sha-a1b2c3d | Best of both; supply-chain audit trail |
For most AI agent fleets, date-based + git SHA in image labels is the right choice — easy to reason about, easy to rollback, captures the source-of-truth.
Rollback procedure
Identify the bad image
Audit log shows which image is in use. CloudWatch alarms or operator-noticed quality dip identify the bad version.
Update workspace default
Change the workspace's default image tag in the dashboard from the bad version to the previous-known-good version. New sandboxes use the rolled-back image immediately.
Recycle running sandboxes
Existing running sandboxes still use the bad image. Recycle them (rolling restart) to pick up the rolled-back version. The platform supports staged rollout: 10% / 50% / 100% over a few hours.
Investigate and fix
Don't just delete the bad tag — keep it so post-incident investigation can reproduce. Investigate; fix; push a new tag; ship.
Image-bloat hygiene
Bigger images = slower pulls = slower cold starts. Keep yours under 2 GB if possible. Tactics:
Multi-stage builds
Separate the build environment from the runtime environment. Compile in stage 1 (with build tools); copy artifacts to stage 2 (slim runtime).
# Stage 1: builder (heavy) FROM python:3.12 AS builder RUN apt-get update && apt-get install -y build-essential cargo COPY requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt # builder image is ~1.5 GB; we don't ship it # Stage 2: runtime (slim) FROM python:3.12-slim COPY --from=builder /root/.local /home/agent/.local ENV PATH="/home/agent/.local/bin:$PATH" # final image is ~300 MB — only the deps, no build tools
Result: a 5× smaller image with the same runtime capabilities.
Layer discipline
- Combine RUN layers with
&&chains. EachRUNis a layer; many small layers bloats the image. - Clean apt/yum caches in the same RUN that installs.
apt-get install ... && rm -rf /var/lib/apt/lists/* --no-cache-diron pip and equivalents on npm.- Don't copy the world; use a
.dockerignoreto excludenode_modules,.git,__pycache__,.pytest_cache, etc. - Order layers by stability — system packages first, then language deps, then app code. Cache hits maximize.
Real-world bloat measurements
| Without hygiene | With hygiene | Savings |
|---|---|---|
| FROM python:3.12 (full): 1100 MB | FROM python:3.12-slim: 130 MB | 970 MB |
| apt without cleanup: +400 MB | apt with cleanup: +0 MB after | 400 MB |
| pip without --no-cache-dir: +200 MB | pip with --no-cache-dir: +0 MB | 200 MB |
| Single stage: 2.5 GB | Multi-stage: 800 MB | 1.7 GB |
CVE scanning workflow
Custom images age. The base image gets CVE patches; your custom image won't unless you rebuild. Three layers of CVE defense:
Build-time scanning
Trivy in CI (shown above) catches known CVEs before push. Fail the build on CRITICAL or HIGH severity; warn on MEDIUM.
Registry scanning
ECR scans every image on push (if you enable it). Re-scans periodically for newly-disclosed CVEs. The dashboard surfaces vulnerable images so you know which need rebuild.
resource "aws_ecr_repository" "agent" { name = "agent" image_tag_mutability = "IMMUTABLE" image_scanning_configuration { scan_on_push = true } encryption_configuration { encryption_type = "KMS" kms_key = aws_kms_key.images.arn } } resource "aws_ecr_registry_scanning_configuration" "continuous" { scan_type = "ENHANCED" rule { scan_frequency = "CONTINUOUS_SCAN" repository_filter { filter = "agent*" filter_type = "WILDCARD" } } }
Scheduled rebuilds
Even without code changes, rebuild weekly to pick up base-image patches. The cron in the GitHub Actions workflow above does this. For high-security workloads, daily.
Pairing with warm pools (the production pattern)
Warm pools + custom images is the production pattern. Pool members are pre-launched from your custom image; the agent's tools are baked in; claim is sub-second; first action happens immediately.
| Layer | What it does | Cost |
|---|---|---|
| Custom image | Cuts cold-start from 120 sec to 30 sec | Image storage: ~$0.10/GB-month per region |
| Warm pool | Cuts claim time from 30 sec to 0.8 sec | $0.1/hr per pool member idle |
| Combined | 120 sec → 0.8 sec; 150× faster | Pool insurance + image storage |
Read more in Warm Pools. The combo is what high-volume customer-facing agents use.
Cost math at scale
| Scenario | Cold-start time | Cost / 1000 runs (ab0t.medium) |
|---|---|---|
| Default image + install on demand | 120 sec | $13.30 cold-start tax |
| Custom image, pre-baked | 30 sec | $3.30 cold-start tax |
| Custom image + warm pool of 5 | 0.8 sec | ~$0.50/hr pool + $0.30/day for spikes |
For agencies running <100 runs/day, custom images probably aren't worth the maintenance. For platforms running 1000+ runs/day, they save $8-10/day and remove startup variability — usually a clear win.
Image storage cost
ECR charges $0.10/GB-month for storage. A 1.5 GB image = $0.15/month. Even for fleets with 20 different role-images: ~$3/month total. The cost is the time-to-build and the CI minutes, not the storage.
Real-world patterns from the field
Pattern 1: Single base image, many role overlays
A SaaS startup. One agent-base:vX.Y.Z image with shared tools (Claude Code, common Python deps, internal CLI). Per-role images (paralegal:vX.Y.Z, support:vX.Y.Z, finance:vX.Y.Z) layered on top with role-specific tools. Base updates monthly; role overlays update as needed. 6 role-images, ~$5/month total storage, all built from the same CI pipeline.
Pattern 2: Customer-specific images for SaaS multi-tenancy
A vertical-AI SaaS. Per-customer Docker images that include their specific integrations and CLAUDE.md. ~50 customers, ~50 images, all built from the same CI with templated Dockerfiles. Each customer's image references their specific service-account configurations. Storage cost ~$50/month total.
Pattern 3: GPU + ML stack pre-baked
An ML team. Custom GPU image based on nvidia/cuda:12.4-runtime-ubuntu22.04 with PyTorch, transformers, and team's internal model-serving stack. 4.8 GB image; 90 sec to pull on first launch but cached after. Saves 6+ minutes of CUDA-driver-pinning on every cold start. Critical for ML workloads where a missed driver version means workload doesn't run at all.
Pattern 4: Auto-rebuild on dependency security advisory
An agency. CI workflow watches dependabot security advisories; on a HIGH or CRITICAL advisory affecting the image, triggers an out-of-band rebuild. Combined with weekly scheduled rebuilds, image stays current. Quarterly review of vulnerability backlog confirms no stale CVEs.
Pattern 5: Pinned model-binary version for reproducibility
A finance team running compliance-sensitive agents. Image pins specific model-API client library versions; specific CLI versions; specific Python and Node minor versions. When a regulator asks "what software was on the box when this report was produced?", the image SHA + manifest answers exactly.
Anti-patterns: what not to do
- Using
:latestin production. Reproducibility goes out the window. - Baking secrets into the image. Image is shareable; secrets aren't. Inject at runtime.
- One giant Dockerfile for all roles. Coupling forces rebuilds for unrelated changes. Use base + role-overlay.
- Skipping multi-stage builds. Single-stage images carry build tools into production unnecessarily.
- Not pinning dependency versions.
pip install requestsvspip install requests==2.31.0— the unpinned version installs whatever's latest at build time, breaking reproducibility. - Letting images age without rebuilds. 6 months without a rebuild = 6 months of accumulated CVEs.
- Building manually instead of in CI. Manual builds aren't reproducible; CI is.
- Not testing the image after build. Smoke test catches breakage before it hits production sandboxes.
- Storing the image only in one region. Cross-region pulls are slow; replicate to all regions you deploy in.
- Confusing the image with the agent's CLAUDE.md. Image is tools; CLAUDE.md is prompts. Don't bake CLAUDE.md if it changes weekly — mount it instead.
Comparison vs platform alternatives
| Platform | Custom-image story |
|---|---|
| Sandbox Platform | Bring-your-own image from any OCI registry; ECR cross-account; per-workspace image policy; staged rollout; CVE scanning integration |
| E2B | Pre-baked templates + custom Dockerfile builds via E2B's build pipeline |
| Modal | Image-as-code in Python; Modal builds and caches; less Docker-native |
| Daytona | devcontainer.json-based environments; Docker-compatible |
| Browserbase | Pre-baked browser images; custom-image less common (browsers are the unit) |
For platform engineers comfortable with Docker / ECR, Sandbox Platform's "bring your own image" is the most flexible. For teams that want image-as-code with less Docker friction, Modal's approach is friendlier. Pick by ecosystem and team preference.
Frequently asked questions
Can I use Docker Hub instead of ECR?
Yes — any OCI-compatible registry the platform's IAM can authenticate to. ECR is the default for AWS-hosted workspaces. Docker Hub / GHCR / GCR / Quay all supported with proper credentials.
What happens if my image is broken?
Sandbox provisioning fails fast with a "image pull / start" error in the audit log. The platform's blue-green pattern means a bad image doesn't take down running sandboxes. Roll back to a previous tag.
How big can the image be?
Up to 8 GB practical. Above that, pull time dominates start time. Most production images are 1.5-3 GB.
Can I have different images for different agent roles?
Yes — the typical pattern. my-paralegal:v..., my-research:v..., my-support:v.... Common pattern: one base image with shared tools, one role image per role.
How do I keep the image in sync with my CLAUDE.md?
Two patterns. (1) Bake CLAUDE.md into the image — image is self-contained but rebuild needed for every CLAUDE.md change. (2) Mount CLAUDE.md from workspace settings at runtime — image stays static. For most teams, pattern 2 is right.
What about security patching for my custom image?
Custom images age. Ship a CI rebuild on a regular cadence — weekly is generous; monthly minimum. Your registry should scan for CVEs (ECR scanning, Snyk, Trivy).
Can multiple sandbox sizes use the same image?
Yes — the image is independent of sandbox size. The same image runs on ab0t.micro, ab0t.medium, ab0t.gpu.
How do I cache pip/npm packages across image rebuilds?
Three techniques: order Dockerfile RUN steps by stability; use BuildKit cache mounts (RUN --mount=type=cache,target=/root/.cache/pip ...); use a private package mirror.
Should I use Alpine or Debian/Ubuntu base?
For AI agents in 2026: Debian/Ubuntu. Alpine's musl libc breaks binary wheels for many ML / data libraries. Debian-slim is the right balance.
Do I need to sign my Docker images?
For most workloads, no. For supply-chain-sensitive deployments (FedRAMP, financial regulators), use cosign or Notation.
How do I share an image across multiple workspaces?
Push to a registry both workspaces can read. ECR cross-account access is the AWS-native pattern.
Can I update an image without restarting all sandboxes?
New image only applies to new sandbox launches. Recycle sandboxes (rolling restart) to pick up the new image. The platform supports staged rollout with canary recycling.
How do I keep secrets out of the image?
Never bake secrets. Three patterns: env vars at sandbox-create time; mount from a vault; IAM roles for AWS access.
What if my agent harness needs a specific OS or kernel version?
Pick a base image that ships with what you need. Anthropic Computer Use needs Linux desktop env. ML workloads need nvidia/cuda. Most agent harnesses work on standard Debian/Ubuntu.
How does the platform pull my private image at sandbox-launch?
The platform uses an IAM role with ECR pull permissions on your registry. Configure once at workspace level. For non-ECR registries, configure a docker-config secret in workspace settings.