[FEATURE] Docker support in Claude Code web environment

Status Fixed / completed
Maintainer reply None cached
Activity 5 comments · opened Feb 28, 2026 · closed Mar 29, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

When using Claude Code in the web (cloud) environment, Docker is not available as an executable tool. This is a significant limitation for projects that rely on Docker to manage services, run integration tests, or replicate production-like environments locally.

For example, in projects that use docker-compose to orchestrate services — such as databases (PostgreSQL), message brokers (Kafka), CDC connectors (Debezium), schema registries, and stream processors — there is no way to start, stop, rebuild, or inspect those services from within a Claude Code web session. This means Claude Code cannot:

  • Run my test suit of integration tests after adding or modifying the source code of my project
  • Start dependent services before running integration tests
  • Verify that container definitions (docker-compose.yml, Dockerfile) are correct
  • Inspect running containers to diagnose infrastructure issues
  • Rebuild images after dependency or configuration changes

The current workaround requires the developers to manually manage Docker on their local machine in a separate terminal, context-switching away from the Claude Code session and breaking the integrated development flow.

Proposed Solution

Make the docker and docker compose CLI tools available and executable within the Claude Code web environment. Specifically:

  • The docker CLI should be accessible from Bash tool calls so Claude can run commands such as docker compose up, docker compose down, docker ps, docker logs <container>, and docker compose up --build.
  • Container networking should be accessible so that services started via Docker are reachable by the backend under test (e.g., a PostgreSQL container on port 5432).
  • Session-scoped lifecycle: containers started during a session should be cleaned up automatically when the session ends, to avoid resource leaks.

The ideal user experience:

  1. Claude Code web session starts
  2. Claude runs docker compose up -d to bring up backing services
  3. Claude runs the project's test suite against those live services
  4. Claude interprets results and makes changes
  5. On session end, containers are stopped and removed automatically

Alternative Solutions

  • Local Claude Code CLI — running Claude Code locally gives full access to the host Docker daemon. This works but loses the convenience and accessibility of the web interface.
  • Manual coordination — the developer keeps services running in a separate terminal and tells Claude to assume they are available. Error-prone and breaks the self-contained nature of a session.
  • testcontainers managed by pytest — some test frameworks can spin up Docker containers programmatically within tests. This partially mitigates the problem for integration tests but still requires Docker to be present in the execution environment.

Priority

High - Significant impact on productivity

Feature Category

CLI commands and flags

Use Case Example

  1. I open a Claude Code web session on a project that uses Docker Compose to run PostgreSQL, Kafka, Debezium, and a Kafka Streams processor.
  2. I ask Claude to add a new domain feature and run the full test suite including integration tests.
  3. Claude attempts docker compose up -d — the command fails because Docker is not available in the web environment.
  4. Integration tests that rely on a live database and Kafka broker cannot run.
  5. Claude is unable to verify the implementation end-to-end and must ask the developer to manually start services and re-run tests.

With Docker available, Claude could complete the entire workflow autonomously: start services, run tests, observe failures, fix code, and confirm correctness — all within a single uninterrupted session.

Additional Context

This limitation was identified while working on a private project that uses the following Docker Compose service topology:

| Service | Port | Purpose |
|---------|------|---------|
| backend | 8000 | FastAPI application |
| database | 5432 | PostgreSQL 16 (Debezium image for CDC) |
| kafka | 9092 | Message broker |
| schema-registry | 8085 | AVRO schema management |
| debezium | 8083 | CDC connector |
| kstream | 8084 | Kafka Streams processor node |

Without Docker, Claude Code can only run unit tests (which use in-memory fakes). It cannot exercise the full event-driven pipeline or validate any infrastructure configuration changes.

A sandboxed Docker-in-Docker (DinD) or rootless container runtime (e.g., Podman) would be an acceptable implementation approach if full Docker daemon access raises security concerns.

View original on GitHub ↗

5 Comments

ja-ka · 5 months ago

With the help of Claude Code we got it working. Claude Code generated some instructions how to set it up:

Docker & Testcontainers in Constrained Environments

Problem

Claude Code web sessions (and similar constrained CI environments) run on Linux kernel 4.4.0 in a containerized environment where Docker is not pre-installed. This makes it impossible to use Testcontainers for integration testing (MongoDB, Redis, etc.) without manual setup.

Key Constraints & Workarounds

| Constraint | Error Message | Workaround |
|---|---|---|
| Kernel 4.4.0 | failed to convert whiteout file: operation not permitted | Use --storage-driver=vfs instead of overlay2 |
| No iptables/ip6tables | failed to advertise addresses: operation not supported | Use --iptables=false --ip6tables=false |
| No bridge networking | network bridge not found / Testcontainers fails | Use --network host and start containers via Docker CLI directly |
| Stale docker0 interface | existing interface docker0 is not a bridge | Delete with ip link delete docker0 before starting dockerd |

Solution: Automated Docker Setup in Session Start Hook

Add Docker setup to your session start hook (e.g. .claude/hooks/session-start.sh):

# ── Docker Auto-Setup for Constrained Environments ──
if ! command -v docker &>/dev/null || ! docker info &>/dev/null 2>&1; then
  echo "[docker-setup] Docker not available, attempting setup..."
 
  # Clean stale docker0 interface (prevents daemon startup failure)
  if ip link show docker0 &>/dev/null 2>&1; then
    ip link delete docker0 2>/dev/null || true
  fi
 
  # Start dockerd with constrained-environment flags:
  #   --storage-driver=vfs    : works on kernel 4.4.0 (overlay2 needs newer kernel)
  #   --iptables=false        : kernel doesn't support iptables in containers
  #   --ip6tables=false       : same for IPv6
  dockerd \
    --iptables=false \
    --ip6tables=false \
    --storage-driver=vfs \
    &>/tmp/dockerd.log &
 
  # Wait for Docker to be ready (up to 30s)
  for i in $(seq 1 30); do
    if docker info &>/dev/null 2>&1; then
      echo "[docker-setup] Docker ready after ${i}s"
      break
    fi
    sleep 1
  done
 
  if ! docker info &>/dev/null 2>&1; then
    echo "[docker-setup] Docker failed to start. Check /tmp/dockerd.log"
  fi
fi

Why Testcontainers Won't Work Directly

Testcontainers relies on Docker bridge networking to map random host ports to container ports. Without bridge networking (which requires iptables kernel support), Testcontainers cannot:

  1. Create the default bridge network
  2. Assign IP addresses to containers
  3. Map random ports from host to container

The Testcontainers library's UnixSocketStrategy and ConfigurationStrategy both fail because they assume bridge networking is available.

Workaround: Bypass Testcontainers with Host Networking

Instead of using Testcontainers, start containers manually with --network host and fixed ports, then write config files that your test setup reads.

Step 1: Start Containers

# Pull images in parallel
docker pull mongo:7 &
docker pull redis:7-alpine &
wait
 
# Start with host networking on fixed ports
# (host networking bypasses bridge/iptables entirely)
docker run --rm --network host -d --name mongo-testcontainer mongo:7 --port 27099
docker run --rm --network host -d --name redis-testcontainer redis:7-alpine --port 6399
 
# Wait for containers to be healthy
for i in $(seq 1 30); do
  if docker exec mongo-testcontainer mongosh --port 27099 --eval "db.runCommand({ping:1})" &>/dev/null; then
    echo "MongoDB ready"
    break
  fi
  sleep 1
done
 
for i in $(seq 1 15); do
  if docker exec redis-testcontainer redis-cli -p 6399 ping 2>/dev/null | grep -q PONG; then
    echo "Redis ready"
    break
  fi
  sleep 1
done

Step 2: Write Container Info Files

# Write container info for Jest global-setup to consume
echo '{"mongoUri":"mongodb://localhost:27099"}' > packages/plugin-mongodb/.container-info.json
echo '{"redisUri":"redis://localhost:6399"}' > packages/plugin-redis/.container-info.json

Step 3: Update Jest Global Setup

In your Jest global-setup.js, add an early return when .container-info.json exists:

const fs = require("fs");
const path = require("path");
 
const CONTAINER_INFO_PATH = path.resolve(__dirname, ".container-info.json");
 
module.exports = async function globalSetup() {
  // If container-info.json already exists (e.g., from session-start hook),
  // skip Testcontainers startup entirely.
  if (fs.existsSync(CONTAINER_INFO_PATH)) {
    console.log("Using pre-existing container from .container-info.json");
 
    // Optional: clean databases for test isolation when reusing containers
    // (important for MongoDB where state persists between test runs)
    const info = JSON.parse(fs.readFileSync(CONTAINER_INFO_PATH, "utf-8"));
    // ... cleanup logic here
 
    return;
  }
 
  // Normal Testcontainers startup for environments where Docker bridge works
  const { MongoDBContainer } = require("@testcontainers/mongodb");
  const container = await new MongoDBContainer("mongo:7").start();
  // ... rest of normal setup
};

Step 4: Update Jest Global Teardown

Don't delete .container-info.json when using pre-existing containers:

module.exports = async function globalTeardown() {
  const container = globalThis.__CONTAINER__;
 
  if (container) {
    // Only stop and clean up if WE started the container
    await container.stop();
 
    if (fs.existsSync(CONTAINER_INFO_PATH)) {
      fs.unlinkSync(CONTAINER_INFO_PATH);
    }
  }
  // If container was pre-existing, leave .container-info.json
  // so subsequent test runs can reuse it
};

Database Cleanup for Test Isolation

When reusing persistent containers, you need to clean up between test runs:

MongoDB

const mongoose = require("mongoose");
const info = JSON.parse(fs.readFileSync(CONTAINER_INFO_PATH, "utf-8"));
const conn = await mongoose.createConnection(info.mongoUri).asPromise();
const adminDb = conn.db.admin();
const { databases } = await adminDb.listDatabases();
 
for (const db of databases) {
  if (!["admin", "local", "config"].includes(db.name)) {
    await conn.db.client.db(db.name).dropDatabase();
  }
}
 
await conn.close();

Redis

Redis doesn't typically need cleanup between runs since Testcontainers tests usually use isolated key prefixes, but you can flush if needed:

const Redis = require("ioredis");
const info = JSON.parse(fs.readFileSync(CONTAINER_INFO_PATH, "utf-8"));
const client = new Redis(info.redisUri);
await client.flushall();
await client.quit();

Performance Notes

  • vfs storage driver is significantly slower than overlay2 for image pulls and container creation. Image pulls that take 5s with overlay2 may take 30-60s with vfs.
  • First run will be slow due to image pulls. Subsequent runs reuse cached images.
  • Host networking has zero overhead compared to bridge networking.
  • Consider pulling images in parallel to minimize startup time.

Troubleshooting

Docker daemon won't start

# Check logs
cat /tmp/dockerd.log
 
# Common fix: kill stale dockerd
pkill -9 dockerd
rm -f /var/run/docker.pid /var/run/docker.sock
# Then retry startup

Container starts but tests can't connect

# Verify container is running
docker ps
 
# Check if port is listening (host networking)
ss -tlnp | grep 27099  # MongoDB
ss -tlnp | grep 6399   # Redis
 
# Test connectivity
docker exec mongo-testcontainer mongosh --port 27099 --eval "db.runCommand({ping:1})"
docker exec redis-testcontainer redis-cli -p 6399 ping

Testcontainers error: "Could not find a working container runtime strategy"

This means Testcontainers can't use Docker at all. Either Docker isn't running or bridge networking isn't available. Use the manual container approach described above.

ja-ka · 5 months ago

Here is what Claude Code added to session-start.sh:

# ─── Docker Setup for Claude Code Web Sessions ───────────────────────────
# In web/containerized environments (kernel < 4.11), Docker may not be running.
# Start dockerd with vfs storage driver (no overlay needed) and launch
# MongoDB + Redis containers with host networking for Testcontainers tests.
if command -v dockerd &>/dev/null && ! docker info &>/dev/null 2>&1; then
    log_info "Docker not running — starting dockerd (vfs storage driver)..."

    # Clean up stale docker0 bridge interface if present
    ip link delete docker0 2>/dev/null || true

    # Start Docker daemon:
    # --iptables=false / --ip6tables=false: kernel 4.4.0 doesn't support iptables in containers
    # --storage-driver=vfs: overlay2 fails on old kernels, vfs always works (slower but compatible)
    dockerd --iptables=false --ip6tables=false --storage-driver=vfs &>/tmp/dockerd.log &

    # Wait for Docker daemon to be ready (up to 15s)
    for i in $(seq 1 15); do
        if docker info &>/dev/null 2>&1; then
            log_info "Docker daemon ready (attempt $i)"
            break
        fi
        sleep 1
    done

    if docker info &>/dev/null 2>&1; then
        # Pull required images (may already be cached from previous sessions)
        log_info "Pulling test container images..."
        docker pull mongo:7 &>/dev/null &
        MONGO_PULL_PID=$!
        docker pull redis:7-alpine &>/dev/null &
        REDIS_PULL_PID=$!
        wait $MONGO_PULL_PID 2>/dev/null || log_warn "Failed to pull mongo:7"
        wait $REDIS_PULL_PID 2>/dev/null || log_warn "Failed to pull redis:7-alpine"

        # Start containers with host networking (bridge networking needs iptables)
        # Use custom ports to avoid conflicts with any local services
        MONGO_PORT=27099
        REDIS_PORT=6399

        # Stop any existing test containers
        docker stop mongo-testcontainer redis-testcontainer 2>/dev/null || true

        log_info "Starting MongoDB container on port $MONGO_PORT..."
        if docker run --rm --network host -d --name mongo-testcontainer mongo:7 --port $MONGO_PORT &>/dev/null; then
            # Write container-info.json for plugin-mongodb integration tests
            echo "{\"mongoUri\":\"mongodb://localhost:$MONGO_PORT/?directConnection=true\"}" > "$PROJECT_ROOT/packages/plugin-mongodb/.container-info.json"
            log_info "MongoDB container started"
        else
            log_warn "Failed to start MongoDB container"
        fi

        log_info "Starting Redis container on port $REDIS_PORT..."
        if docker run --rm --network host -d --name redis-testcontainer redis:7-alpine --port $REDIS_PORT &>/dev/null; then
            # Write container-info.json for plugin-redis integration tests
            echo "{\"redisUri\":\"redis://localhost:$REDIS_PORT\"}" > "$PROJECT_ROOT/packages/plugin-redis/.container-info.json"
            log_info "Redis container started"
        else
            log_warn "Failed to start Redis container"
        fi

        # Write container-info.json for E2E tests (needs both URIs)
        if [ -f "$PROJECT_ROOT/packages/plugin-mongodb/.container-info.json" ] && [ -f "$PROJECT_ROOT/packages/plugin-redis/.container-info.json" ]; then
            echo "{\"mongoUri\":\"mongodb://localhost:$MONGO_PORT/?directConnection=true\",\"redisUri\":\"redis://localhost:$REDIS_PORT\"}" > "$PROJECT_ROOT/e2e-tests/.container-info.json"
            log_info "E2E test container info written"
        fi

        # Wait for containers to be healthy
        sleep 3
    else
        log_warn "Docker daemon failed to start — integration tests will be skipped"
    fi
fi
# ─── End Docker Setup ────────────────────────────────────────────────────
kolodkin · 5 months ago

+1

juanluiscr27 · 5 months ago

Thank you @ja-ka for the workaround. I asked Claude to implement and similar solution, but adapted for my project. I have tested for a few weeks and it works.

I am going to post my scripts here just in case anyone is using Postgres and Python, like me.

This how my start-session.sh look like:

#!/usr/bin/env bash
set -euo pipefail

# ── Docker Auto-Setup for Constrained Environments ──
# Adapted from ja-ka's workaround (GitHub issue anthropics/claude-code#29515)
#
# This hook runs at Claude Code session start. It detects constrained
# environments (no bridge networking, no iptables) and pre-starts a
# PostgreSQL container with host networking so integration/e2e tests
# can run without testcontainers' bridge-dependent setup.
#
# In normal environments (CI/CD, local dev) this script is a no-op:
# Docker works fine and testcontainers handles everything.

CONTAINER_NAME="postgres-testcontainer"
POSTGRES_PORT=5499
POSTGRES_USER="test"
POSTGRES_PASSWORD="test"
POSTGRES_DB="test"
POSTGRES_IMAGE="debezium/postgres:16"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
CONTAINER_INFO_PATH="$PROJECT_ROOT/backend/.container-info.json"
SCHEMA_FILE="$PROJECT_ROOT/backend/tests/resources/test_schema.sql"

# ── Helper ──
log() { echo "[session-start] $*"; }

# ── Guard: skip if Docker is fully functional ──
if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
    # Docker daemon is running — check if bridge networking works
    if docker network ls 2>/dev/null | grep -q bridge; then
        log "Docker is fully functional (bridge networking available). Skipping workaround."
        exit 0
    fi
    log "Docker daemon running but bridge networking unavailable. Applying workaround."
else
    log "Docker not available. Attempting constrained-environment setup."

    # Clean stale docker0 interface
    if ip link show docker0 &>/dev/null 2>&1; then
        ip link delete docker0 2>/dev/null || true
    fi

    # Start dockerd with constrained-environment flags
    dockerd \
        --iptables=false \
        --ip6tables=false \
        --storage-driver=vfs \
        &>/tmp/dockerd.log &

    # Wait for Docker to be ready (up to 30s)
    for i in $(seq 1 30); do
        if docker info &>/dev/null 2>&1; then
            log "Docker ready after ${i}s"
            break
        fi
        sleep 1
    done

    if ! docker info &>/dev/null 2>&1; then
        log "Docker failed to start. Check /tmp/dockerd.log. Skipping."
        exit 0
    fi
fi

# ── Guard: skip if container already running ──
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then
    log "Container '$CONTAINER_NAME' already running. Skipping."
    exit 0
fi

# ── Pull image ──
log "Pulling $POSTGRES_IMAGE..."
docker pull "$POSTGRES_IMAGE" 2>/dev/null || {
    log "Failed to pull image. Skipping workaround."
    exit 0
}

# ── Start PostgreSQL with host networking ──
log "Starting PostgreSQL on port $POSTGRES_PORT with host networking..."
docker run --rm --network host -d \
    --name "$CONTAINER_NAME" \
    -e POSTGRES_USER="$POSTGRES_USER" \
    -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
    -e POSTGRES_DB="$POSTGRES_DB" \
    "$POSTGRES_IMAGE" \
    -p "$POSTGRES_PORT" || {
    log "Failed to start container. Skipping workaround."
    exit 0
}

# ── Wait for PostgreSQL to accept connections ──
log "Waiting for PostgreSQL to accept connections..."
for i in $(seq 1 30); do
    if docker exec "$CONTAINER_NAME" pg_isready -h localhost -p "$POSTGRES_PORT" -U "$POSTGRES_USER" &>/dev/null; then
        log "PostgreSQL ready after ${i}s"
        break
    fi
    sleep 1
done

if ! docker exec "$CONTAINER_NAME" pg_isready -h localhost -p "$POSTGRES_PORT" -U "$POSTGRES_USER" &>/dev/null; then
    log "PostgreSQL failed to start. Cleaning up."
    docker stop "$CONTAINER_NAME" 2>/dev/null || true
    exit 0
fi

# ── Initialize test schema ──
if [ -f "$SCHEMA_FILE" ]; then
    log "Initializing test schema from $SCHEMA_FILE..."
    docker exec -i "$CONTAINER_NAME" \
        psql -h localhost -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
        < "$SCHEMA_FILE" &>/dev/null || {
        log "Warning: schema initialization failed. Tests may fail."
    }
else
    log "Warning: schema file not found at $SCHEMA_FILE"
fi

# ── Write container info marker ──
cat > "$CONTAINER_INFO_PATH" <<EOF
{
    "host": "localhost",
    "port": $POSTGRES_PORT,
    "user": "$POSTGRES_USER",
    "password": "$POSTGRES_PASSWORD",
    "dbname": "$POSTGRES_DB"
}
EOF

log "Container info written to $CONTAINER_INFO_PATH"
log "PostgreSQL workaround setup complete."

Then, in .claude/settings.json The hook is registered like this:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/session-start.sh"
          }
        ]
      }
    ]
  }
}

And finally, my conftest.py needs some special wire-up to distinguish a local session Docker from a Web environment one.

import json
from pathlib import Path

from fastapi.testclient import TestClient
from psycopg_pool import ConnectionPool
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from testcontainers.postgres import PostgresContainer

CONTAINER_INFO_PATH = Path(__file__).parent.parent / '.container-info.json'

_postgres_container: PostgresContainer | None = None


def use_preexisting_container() -> bool:
    """Check if a pre-existing PostgreSQL container was started by the session-start hook."""
    return CONTAINER_INFO_PATH.exists()


def get_postgres_container() -> PostgresContainer:
    """Lazily create and return the testcontainers PostgresContainer instance."""
    global _postgres_container
    if _postgres_container is None:
        from testcontainers.postgres import PostgresContainer as _PostgresContainer
        _postgres_container = _PostgresContainer('debezium/postgres:16')
    return _postgres_container


def get_preexisting_connection_url(driver: str | None = 'psycopg') -> str:
    """Build a connection URL from the container info file written by the session-start hook."""
    with CONTAINER_INFO_PATH.open() as f:
        info: dict[str, object] = json.load(f)
    host = info['host']
    port = info['port']
    user = info['user']
    password = info['password']
    dbname = info['dbname']
    if driver:
        return f'postgresql+{driver}://{user}:{password}@{host}:{port}/{dbname}'
    return f'postgresql://{user}:{password}@{host}:{port}/{dbname}'


@pytest.fixture(scope='module')
def test_engine():
    if use_preexisting_container():
        connection_url = get_preexisting_connection_url(driver='psycopg')
        engine = create_engine(connection_url)

        DBModelBase.metadata.create_all(bind=engine)

        yield engine
    else:
        container = get_postgres_container()
        script = Path(__file__).parent.parent / 'resources' / 'test_schema.sql'
        container.with_volume_mapping(
            host=str(script), container=f'/docker-entrypoint-initdb.d/{script.name}',
        )
        container.start()

        connection_url = container.get_connection_url(driver='psycopg')

        engine = create_engine(connection_url, json_serializer=json_serializer)

        DBModelBase.metadata.create_all(bind=engine)

        yield engine

        container.stop()


@pytest.fixture
def empty_test_db_session(test_engine):
    test_session_local = sessionmaker(
        autocommit=False, autoflush=False, bind=test_engine,
    )

    with test_session_local.begin() as session:
        for table in reversed(DBModelBase.metadata.sorted_tables):
            session.execute(table.delete())

        yield session

I hope this help. 🚀

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.