Generating Dockerfile Configurations with Kiro
Kiro for Dockerfiles: Generating Production-Ready Container Configurations
This post walks through asking Kiro to generate a Dockerfile for a Python FastAPI application, examining every decision it makes, and then asking it to improve the result for specific concerns. The app itself is intentionally simple - the focus is entirely on the container configuration.
The Setup
The application is a Python FastAPI service with the following structure:
1
2
3
4
5
my-api/
├── src/
│ └── main.py
├── requirements.txt
└── Dockerfile ← we're generating thisrequirements.txt contains:1
2
3
fastapi==0.111.0
uvicorn[standard]==0.29.0
pydantic==2.7.1And
src/main.py is a minimal API:1
2
3
4
5
6
7
8
9
10
11
from fastapi import FastAPI
app = FastAPI()
def health():
return {"status": "ok"}
def root():
return {"message": "Hello from FastAPI"}The Prompt
Here's exactly what was typed into Kiro:
"Generate a production-ready Dockerfile for this Python FastAPI app. It serves HTTP on port 8000, dependencies are in requirements.txt, and the entry point is src/main.py. Optimize for security and image size."
What Kiro Generated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# syntax=docker/dockerfile:1
# ── Stage 1: build dependencies ───────────────────────────────────────────────
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build tools needed for packages with C extensions
RUN apt-get update \
&& apt-get install --no-install-recommends -y gcc libffi-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy only the requirements file first to maximize layer cache reuse
COPY requirements.txt ./
# Install into an isolated prefix so we can copy cleanly in the next stage
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir --prefix=/install -r requirements.txt
# ── Stage 2: production runtime ───────────────────────────────────────────────
FROM python:3.12-slim AS runtime
WORKDIR /app
# Create a non-root system user — no home directory, no shell, no password
RUN groupadd --system --gid 1001 appgroup \
&& useradd --system --uid 1001 --gid appgroup --no-create-home appuser
# Pull only the installed packages from the builder — no gcc, no apt cache
COPY --from=builder --chown=appuser:appgroup /install /usr/local
# Copy application source
COPY --chown=appuser:appgroup src/ ./src/
# Drop to non-root before the process starts
USER appuser
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
PORT=8000
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
# Exec form ensures Python is PID 1 and receives OS signals directly
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]That's a solid Dockerfile. Let's walk through each decision.
Decision 1: Base Image — python:3.12-slim
Kiro chose
python:3.12-slim, not python:3.12. Here's why that matters:| Image | Compressed size | Notes |
|---|---|---|
python:3.12 | ~350 MB | Full Debian, includes compilers and tools |
python:3.12-slim | ~45 MB | Debian without extras — glibc intact |
slim is the pragmatic choice: small enough to matter, compatible enough to not cause pain.The pinned version (
3.12) rather than latest is also intentional. latest is a moving target. A future Python 3.14 release could silently break your application on the next build.Decision 2: Multi-Stage Build
This is the single biggest lever for both image size and attack surface. Kiro used two stages:
Stage 1 (
builder) installs gcc and libffi-dev to compile any C extensions, then installs all Python dependencies.Stage 2 (
runtime) starts from a fresh python:3.12-slim and copies only the installed packages — not the compiler, not the apt package lists, not pip's download cache.The key line is:
1
COPY --from=builder /install /usr/localEverything that was needed to build the dependencies stays in the builder stage and is discarded. The final image never contains
gcc. A typical FastAPI app with pydantic and cryptography might pull in 80+ MB of build tooling that has zero business being in production.Decision 3: Non-Root User
1
2
3
RUN groupadd --system --gid 1001 appgroup \
&& useradd --system --uid 1001 --gid appgroup --no-create-home appuser
USER appuserContainers run as root by default. If an attacker exploits a vulnerability in your application or any of its dependencies, root access inside the container makes privilege escalation significantly easier — writing to system paths, installing tools, or escaping via misconfigured volume mounts all become possible.
The
--system flag creates an account with no home directory, no login shell, and no password. It's a service identity, not a user account. Most security scanners (Trivy, Snyk, Checkov) and Kubernetes PodSecurityStandards will flag root-running containers. Getting this right from the start avoids retrofitting it later.Decision 4: Layer Caching Optimization
1
2
3
4
COPY requirements.txt ./ # copied first
RUN pip install ... # cached until requirements.txt changes
COPY src/ ./src/ # copied after installDocker builds layers incrementally and caches them. If you
COPY . . before running pip install, any change to any .py file invalidates the pip install layer and forces a full reinstall on every build.Dependencies change far less often than source code. By copying
requirements.txt first, the install layer is only invalidated when dependencies actually change. On a busy CI pipeline, this turns a 2-minute build into a 10-second one for most commits.Decision 5: Python Environment Variables
1
2
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1These two are easy to overlook but matter in containers.
PYTHONDONTWRITEBYTECODE=1 stops Python from writing .pyc bytecode files to disk. Inside a container they're wasted space — the source files are already present and the container is ephemeral.PYTHONUNBUFFERED=1 forces stdout and stderr to flush immediately. Without it, Python buffers output. When a container crashes, the last lines of logs — exactly the ones you need — may never be written. This is a silent failure mode that's painful to debug.Decision 6: Exec Form CMD
1
2
3
4
5
# Shell form — don't use this in production
CMD python -m uvicorn src.main:app --host 0.0.0.0 --port 8000
# Exec form — what Kiro generated
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]Shell form wraps the command in
/bin/sh -c, making sh PID 1. Your Python process becomes a child of sh and won't receive SIGTERM from Docker or Kubernetes during graceful shutdown. The container gets killed hard after the grace period instead of draining in-flight requests cleanly.Exec form makes
python PID 1 directly. It receives signals. Graceful shutdown works.Asking Kiro to Improve It
For security
"Improve this Dockerfile for security. Use a distroless base image so there's no shell in the final image."
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM gcr.io/distroless/python3-debian12 AS runtime
WORKDIR /app
COPY --from=builder /install/lib/python3.12/site-packages \
/usr/lib/python3/dist-packages
COPY --from=builder --chown=nonroot:nonroot /app/src ./src
USER nonroot
ENV PYTHONUNBUFFERED=1 PYTHONPATH=/app PORT=8000
EXPOSE 8000
CMD ["src/main.py"]Distroless images contain only the Python interpreter — no shell, no
apt, no curl. There's nothing for an attacker to use interactively after gaining access. The tradeoff is real: docker exec for live debugging no longer works. You need structured logging and external observability in place before adopting this. Kiro flags this tradeoff in its explanation rather than just applying the change silently.For image size
"Minimize the final image size. Strip anything not needed at runtime."
Kiro adds a
.dockerignore:1
2
3
4
5
6
7
8
9
__pycache__
*.pyc
*.pyo
.git
.env*
.venv
tests/
*.md
.pytest_cacheAnd tightens the install step:
1
2
3
4
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt \
&& find /install -type d -name "tests" -exec rm -rf {} + 2>/dev/null; true \
&& find /install -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; true \
&& find /install -name "*.dist-info" -exec rm -rf {} + 2>/dev/null; trueThis strips test directories and package metadata that pip installs alongside packages but that the runtime never uses. A typical FastAPI app drops from ~180 MB to ~140 MB with these additions.
What This Shows
Kiro's output isn't a generic template. It reasoned about the specific app — FastAPI, port 8000,
src/main.py entry point — and applied production defaults that many hand-written Dockerfiles skip: pinned base images, multi-stage separation of build and runtime concerns, non-root execution, signal-correct process management, and a health check wired to the actual /health endpoint.The follow-up prompts show it can reason about tradeoffs too. Distroless improves security but removes the ability to shell into a running container. That's a real operational consideration worth understanding before adopting it — and Kiro explains it rather than just making the change.
For developers who write Dockerfiles regularly, the value isn't that Kiro knows things you don't. It's that it applies all of them consistently, every time, without having to remember the checklist.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article