AI

Build and Debug Docker Containers with Claude Code

Dockerfiles and Compose stacks are where Claude Code earns its keep. The files are structured, the patterns are predictable, and validation is instant: either it builds or it doesn’t. You describe what you want, Claude Code writes the Dockerfile, builds the image, and shows you the result. When a container won’t start, you paste the error and Claude Code traces it to the root cause.

Original content from computingforgeeks.com - post 164930

This guide is part of the Claude Code for DevOps Engineers series. Every demo below ran on a real Docker 29.1.2 installation. The builds, the image sizes, the error messages, the container logs are all captured from actual execution. For the full command reference, see the Claude Code cheat sheet.

Tested March 2026 | Docker 29.1.2, Compose 2.40.3, Python 3.12-alpine, PostgreSQL 17.9, Redis 7, Nginx alpine

What You Need

  • Claude Code installed and authenticated
  • Docker Engine 24+ with Compose V2 (Docker on Rocky Linux 10 or Docker on Ubuntu 24.04)
  • Docker must be accessible without sudo (add your user to the docker group)
  • Note: Docker requires disabling Claude Code’s sandbox mode or adding Docker socket exclusions

Generate a Production Dockerfile

Most engineers write Dockerfiles that work. Fewer write ones that are small, secure, and cache-friendly. Claude Code generates the optimized version by default.

Start with a simple Flask app. Create app/app.py with a JSON health endpoint and app/requirements.txt with flask and gunicorn. Then tell Claude Code:

Create two Dockerfiles for the Flask app in ./app/:
1. A naive single-stage build using python:3.12-slim
2. A production multi-stage build using alpine, non-root user, proper
   layer caching. Build both and compare sizes.

Claude Code generates the naive version first:

FROM python:3.12-slim
WORKDIR /app
COPY app/ .
RUN pip install -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]

Then the production multi-stage version:

# Build stage
FROM python:3.12-alpine AS builder
WORKDIR /build
COPY app/requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Runtime stage
FROM python:3.12-alpine
RUN adduser -D appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY app/ .
USER appuser
EXPOSE 5000
CMD ["gunicorn", "-b", "0.0.0.0:5000", "-w", "2", "app:app"]

It builds both and shows the size comparison:

TAG         SIZE
optimized   88.2MB
naive       227MB

The multi-stage build is 61% smaller. But size isn’t the only win. Claude Code also verifies the security posture:

Naive runs as:     root
Optimized runs as: appuser

The naive image runs as root, which means a container escape gives the attacker root on the host. The optimized image runs as appuser, a non-privileged account. Claude Code adds this by default because it knows Docker security best practices.

A quick test confirms the optimized container works:

docker run -d --name demo-test -p 5050:5000 demo-api:optimized
curl -s http://localhost:5050/
{"host":"5011a306c6c6","python":"3.12.13","service":"demo-api","status":"running"}

curl -s http://localhost:5050/health
{"status":"healthy"}

The container responds on port 5050 with the correct Python version and hostname.

Docker Compose Full Stack

Single containers are simple. The real test is generating a multi-service stack with proper networking, health checks, and persistence. This demo builds a complete backend: Flask app, PostgreSQL database, Redis cache, and Nginx reverse proxy.

Generate a docker-compose.yml with:
- Flask app connecting to PostgreSQL and Redis
- PostgreSQL 17 with a persistent volume and health check
- Redis for caching
- Nginx reverse proxy on port 8080
- Proper networking (all services on a backend network)
- Health checks and depends_on conditions
Build and bring it up.

Claude Code generates the Compose file with all four services, a named volume for PostgreSQL persistence, and a dedicated bridge network. The key details it handles automatically:

  • Health check on PostgreSQL using pg_isready with the correct user and database
  • depends_on with conditions so the app waits for PostgreSQL to be healthy before starting
  • Nginx config with proper proxy headers (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto)
  • Named volume for PostgreSQL data (survives docker compose down)

Bringing up the stack:

docker compose up -d

All four containers start and the health check passes:

NAME                    IMAGE                STATUS                    PORTS
compose-stack-app-1     compose-stack-app    Up 14 seconds             5000/tcp
compose-stack-db-1      postgres:17-alpine   Up 20 seconds (healthy)   5432/tcp
compose-stack-nginx-1   nginx:alpine         Up 14 seconds             0.0.0.0:8080->80/tcp
compose-stack-redis-1   redis:7-alpine       Up 20 seconds             6379/tcp

Testing the endpoint through the Nginx proxy confirms all services are connected:

curl -s http://localhost:8080/ | python3 -m json.tool
{
    "cache": "connected (hits: 1)",
    "database": "PostgreSQL 17.9 on aarch64-unknown-linux-musl",
    "host": "21743ef0d2ee",
    "service": "compose-demo"
}

PostgreSQL 17.9 is connected and responding. Redis is tracking hit counts (incrementing on every request). The entire stack took one prompt to generate and one command to launch. Hitting the endpoint again shows the Redis counter increment to 2, confirming cache persistence across requests.

Debug a Container That Won’t Start

Containers fail silently. They exit, you see “Exited (1)” in docker ps -a, and the only clue is in the logs. Claude Code reads those logs and traces the problem faster than you can type docker logs.

Scenario 1: Wrong entrypoint file

A Dockerfile references server.py but the actual file is app.py. The container exits immediately.

My container demo-api:broken exits immediately. Figure out why.

Claude Code checks the container status and reads the logs:

docker ps -a --filter name=broken-cmd
NAMES        STATUS
broken-cmd   Exited (2) 16 seconds ago

Exit code 2 means the command failed. The logs reveal exactly why:

docker logs broken-cmd
python: can't open file '/app/server.py': [Errno 2] No such file or directory

The Dockerfile CMD references server.py but the actual file is app.py. Claude Code inspects the Dockerfile, finds the mismatch, and fixes the CMD line. One rebuild and the container runs.

Scenario 2: Port already in use

You try to start a container on port 8080 but something else is already listening there.

docker run -d --name port-conflict -p 8080:5000 demo-api:optimized

Docker returns the bind error:

Error response from daemon: failed to set up container networking:
driver failed programming external connectivity on endpoint port-conflict:
Bind for 0.0.0.0:8080 failed: port is already allocated

Tell Claude Code “the container won’t start, port conflict on 8080” and it finds the offender:

docker ps --filter "publish=8080" --format "table {{.Names}}\t{{.Ports}}"
NAMES                   PORTS
compose-stack-nginx-1   0.0.0.0:8080->80/tcp

The Nginx container from the Compose stack is using port 8080. Claude Code removes the failed container and relaunches on a free port:

docker rm port-conflict
docker run -d --name port-fixed -p 5051:5000 demo-api:optimized

Verified working:

NAMES        STATUS         PORTS
port-fixed   Up 2 seconds   0.0.0.0:5051->5000/tcp

These are the two most common container startup failures. The first (wrong file/command) accounts for most “Exited (2)” cases. The second (port conflict) accounts for most “failed to set up networking” errors. Claude Code diagnoses both in under 10 seconds.

Optimize an Existing Dockerfile

When you inherit a Dockerfile that works but produces a bloated image, Claude Code can analyze the layers and suggest improvements. Ask it to compare:

Analyze the layers of demo-api:naive and demo-api:optimized.
Show me where the size difference comes from.

Claude Code runs docker history on both images and compares:

=== Naive image (227MB) ===
SIZE      CREATED BY
18.6MB    RUN pip install -r requirements.txt
16.4kB    COPY app/
44.6MB    RUN set -eux; savedAptMark="$(apt-mark...)"   ← Debian build tools
163MB     Base: python:3.12-slim (Debian)

=== Optimized image (88.2MB) ===
SIZE      CREATED BY
7.12MB    COPY /install /usr/local                       ← only runtime deps
16.4kB    COPY app/
41kB      RUN adduser -D appuser
81MB      Base: python:3.12-alpine (musl libc)

The size difference comes from three sources:

  • Base image: python:3.12-slim (Debian) is 163MB. python:3.12-alpine is 81MB. Alpine uses musl libc instead of glibc, cutting the base in half
  • Build tools: the naive image keeps pip’s cache and any build dependencies in the final image. Multi-stage copies only the installed packages (7.12MB vs 18.6MB)
  • Layer efficiency: the build stage is discarded entirely. Only runtime files make it to the final image

Claude Code suggests these optimizations automatically when you ask it to review a Dockerfile. It catches: missing --no-cache-dir on pip, running as root, missing .dockerignore, unnecessary packages in the runtime image, and layers that could be combined.

Dockerfile Optimization Checklist

After running dozens of Dockerfile generation sessions, these are the patterns Claude Code applies consistently:

PatternWhy It MattersImpact
Multi-stage buildsBuild tools don’t ship to production30-70% size reduction
Alpine or distroless baseMinimal attack surface, smaller image50-80% base size reduction
Non-root USERLimits blast radius of container escapeSecurity
--no-cache-dir on pipPip cache wastes space in the layer5-20MB savings
COPY requirements firstLayer caching: deps rebuild only when requirements changeFaster rebuilds
.dockerignorePrevents .git, __pycache__, .env from entering contextFaster builds, no secrets leaked
Specific image tags:latest breaks reproducibilityReliability
Health checks in ComposeEnsures depends_on waits for readinessNo race conditions

Practical Docker Prompts That Work

After testing Claude Code on dozens of Docker tasks, these prompt patterns produce the best results.

Always specify the language runtime and base image preference. “Create a Dockerfile for my Go app using Alpine” produces better output than “Dockerize this app.” Claude Code picks a sensible default, but your choice of base image matters for compatibility and size.

Ask for the .dockerignore alongside the Dockerfile. Claude Code sometimes forgets to create one. A missing .dockerignore means .git/, __pycache__/, .env, and node_modules all get copied into the build context, which slows builds and can leak secrets into layers.

Request health checks explicitly in Compose files. Claude Code adds them for databases (PostgreSQL, MySQL) but sometimes skips them for application containers. A health check on your app container catches startup crashes before traffic arrives.

For debugging, paste the full error. “My container won’t start” gives Claude Code nothing to work with. “My container exits with code 137” tells it immediately that the container was OOM-killed. Exit code 1 means the app threw an error. Exit code 2 means the command or file wasn’t found. Exit code 127 means a binary is missing. The more specific the error, the faster the diagnosis.

Use Claude Code to review inherited Dockerfiles. Paste an existing Dockerfile and ask “Review this for security issues, size optimization, and caching.” Claude Code catches things like running as root, installing dev dependencies in production, missing HEALTHCHECK instructions, and unnecessary layers.

When Claude Code Gets Docker Wrong

Two areas where you’ll need to override Claude Code’s defaults:

Alpine compatibility. Alpine uses musl libc. Some Python packages with C extensions (numpy, pandas, scikit-learn) fail to install on Alpine or require extra build dependencies. If your app uses data science libraries, use python:3.12-slim instead and accept the size tradeoff. Claude Code sometimes suggests Alpine without checking dependency compatibility.

Compose networking assumptions. Claude Code creates a single flat network by default. For production stacks, you want frontend and backend networks separated so the database is never directly reachable from the reverse proxy. If security matters (it always does), explicitly request network segmentation in your prompt.

Part of the Claude Code for DevOps Series

This Docker guide connects to the broader series. The SSH server management guide covers running Docker commands on remote servers via SSH. The Kubernetes guide covers converting Compose stacks to Kubernetes manifests.

Related Articles

Containers Running PowerDNS and PowerDNS Admin in Docker Containers Containers Manage Multiple Kubernetes Clusters with kubectl and kubectx Containers Stream Desktop and Containerized Applications on Browser Docker Install Docker & Compose on Rocky Linux 9

Leave a Comment

Press ESC to close