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.

Create a free account to save this page, take notes and track your progress. Sign in

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:

  1. Pinned, slim base image (python:3.14-slim).
  2. Dependencies before code, so code edits don't reinstall packages.
  3. A non-root user, created in the image and selected with USER.
  4. Exec-form CMD, so the app is PID 1 and receives SIGTERM.
  5. Configuration from environment variables, not baked-in files.
  6. 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

Build and call a containerized Python API
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.1

Run 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 stop waits 10 seconds and kills the app.
  • Python output buffering hides logs until the buffer fills; set PYTHONUNBUFFERED=1.
  • Binding to 127.0.0.1 inside the container: the app must listen on 0.0.0.0 to 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.

Was this page helpful?