Docker from Basics to Production · Containerize Real Applications
Containerize a Python API
Write a production-shaped Dockerfile for a small Python web API: slim base, dependency layer, non-root user, exec-form command and a health endpoint.
What you'll build
A small Python HTTP API (standard library only, so it builds offline) packaged in an image that is cache-friendly, runs as a non-root user, stops cleanly and exposes a /health endpoint. You'll run it and call it with curl.
The idea
A good application image follows the same shape in every language:
- Pinned, slim base image (
python:3.14-slim). - Dependencies before code, so code edits don't reinstall packages.
- A non-root user, created in the image and selected with
USER. - Exec-form
CMD, so the app is PID 1 and receivesSIGTERM. - Configuration from environment variables, not baked-in files.
- A health endpoint the platform can probe.
Python-specific settings: PYTHONDONTWRITEBYTECODE=1 (no .pyc files in the image) and PYTHONUNBUFFERED=1 (logs appear immediately instead of sitting in a buffer).
The code
trap 'docker rm -f rc-pyapi > /dev/null 2>&1; docker image rm -f rc-pyapi:1 > /dev/null 2>&1' EXIT
mkdir app && cat > app/main.py <<'EOF'
import json, os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
body = {"status": "ok"}
else:
body = {"message": f"hello from {os.environ.get('APP_NAME', 'api')}", "user": os.getuid()}
data = json.dumps(body).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(data)
def log_message(self, fmt, *args):
print("request:", self.requestline, flush=True)
ThreadingHTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8000"))), Handler).serve_forever()
EOF
echo "# no third-party dependencies yet" > requirements.txt
cat > Dockerfile <<'EOF'
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /srv
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN useradd --uid 10001 --no-create-home appuser
COPY app/ ./app/
USER 10001
EXPOSE 8000
CMD ["python", "app/main.py"]
EOF
docker build -q -t rc-pyapi:1 . > /dev/null
docker run -d --name rc-pyapi -p 127.0.0.1:18101:8000 -e APP_NAME=orders rc-pyapi:1 > /dev/null
for i in $(seq 20); do curl -sf localhost:18101/health > /dev/null && break; sleep 0.5; done
curl -s localhost:18101/
echo
curl -s localhost:18101/health
echo
docker logs rc-pyapi
Output
{"message": "hello from orders", "user": 10001}
{"status": "ok"}
request: GET /health HTTP/1.1
request: GET / HTTP/1.1
request: GET /health HTTP/1.1Run this on your computer — see “Get Started” for setup.
Common mistakes
COPY . .before installing dependencies, so every code change reinstalls everything.- Running as root because the base image does.
- Shell-form
CMD python app/main.py: the shell is PID 1,docker stopwaits 10 seconds and kills the app. - Python output buffering hides logs until the buffer fills; set
PYTHONUNBUFFERED=1. - Binding to
127.0.0.1inside the container: the app must listen on0.0.0.0to be reachable through a published port.
Key points
- Slim pinned base, dependencies before code, non-root user, exec-form command.
- Configure through environment variables and expose a health endpoint.
- Listen on 0.0.0.0 inside the container; publish ports on the host.
The tutorial's Writing a Dockerfile page covers each instruction. Further reading: Dockerfile best practices
Practice questions
Why must an app in a container listen on 0.0.0.0 rather than 127.0.0.1?
Answer
127.0.0.1 inside the container is the container's own loopback; traffic arriving through a published port comes in on the container's network interface, so the app must listen on all interfaces.
What do PYTHONUNBUFFERED=1 and PYTHONDONTWRITEBYTECODE=1 do in a container?
Answer
The first makes stdout and stderr unbuffered so logs appear immediately; the second stops Python writing .pyc files, keeping the image and writable layer clean.
Why use exec-form CMD for an API container?
Answer
The process runs as PID 1 and receives SIGTERM from docker stop or Kubernetes, so it can shut down gracefully instead of being killed after the grace period.
Open in the app to mark lessons complete, take notes and track your progress.