🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Python Docker — Containerized Applications

Python DevOpsDocker🟢 Free Lesson

Advertisement

Python Docker — Containerized Applications

Docker packages applications with their dependencies into portable containers. This eliminates "it works on my machine" problems and enables consistent deployments.

Learning Objectives

  • Write efficient Dockerfiles for Python
  • Use multi-stage builds to reduce image size
  • Manage secrets and environment variables in containers
  • Use docker-compose for multi-service applications
  • Understand Docker networking, volumes, and security best practices

Why Docker?

Imagine you built a Python app on your laptop. It works perfectly. But when you deploy it to a server, it breaks because the server has a different Python version, missing system packages, or conflicting dependencies.

Docker solves this by creating a snapshot of your entire application environment — code, dependencies, system libraries, and runtime. This snapshot (called an image) runs the same way everywhere.

Architecture Diagram
Without Docker:           With Docker:
+-------------+          +-------------+
| Your Laptop |          |  Container  |
| Python 3.11 |          | +---------+ |
| pandas 2.0  |   --►   | | Your App| |
| Ubuntu 22   |          | | Python  | |
| Custom libs |          | | deps    | |
+-------------+          | +---------+ |
+-------------+          |  Ubuntu 22   |
|   Server    |          +-------------+
| Python 3.9  |             Same everywhere!
| Missing pkg |
+-------------+

Docker Terminology

TermDefinitionExample
ImageRead-only template with code + dependenciespython:3.11-slim
ContainerRunning instance of an imagedocker run myapp
DockerfileRecipe for building an imageFROM python:3.11
RegistryStorage for imagesDocker Hub, ECR
VolumePersistent data storage-v pgdata:/var/lib/postgresql/data
LayerEach instruction creates a layerCOPY, RUN, ADD

Your First Dockerfile

A Dockerfile is a recipe for building a Docker image. Each line creates a layer — a snapshot of changes.

# Dockerfile

# 1. Start from a base image (Python 3.11 slim)
FROM python:3.11-slim

# 2. Set working directory inside container
WORKDIR /app

# 3. Copy dependency file first (for caching)
COPY requirements.txt .

# 4. Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# 5. Copy application code
COPY . .

# 6. Expose port (documentation — does not actually publish)
EXPOSE 8000

# 7. Command to run when container starts
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Dockerfile Instructions Reference

InstructionPurposeExample
FROMBase imageFROM python:3.11-slim
WORKDIRSet working directoryWORKDIR /app
COPYCopy files from host to imageCOPY . .
RUNExecute a commandRUN pip install -r requirements.txt
CMDDefault command at runtimeCMD ["python", "app.py"]
EXPOSEDocument listening portsEXPOSE 8000
ENVSet environment variablesENV DEBUG=false
ARGBuild-time variablesARG PYTHON_VERSION=3.11
ENTRYPOINTFixed executableENTRYPOINT ["python"]
ADDCopy with auto-extractionADD app.tar.gz /app/

Why This Order Matters

Architecture Diagram
Layer 1: FROM python:3.11-slim          (~150MB, cached)
Layer 2: WORKDIR /app                   (tiny)
Layer 3: COPY requirements.txt .        (small file)
Layer 4: RUN pip install ...            (~200MB, SLOW — cache this!)
Layer 5: COPY . .                       (your code changes often)

If you change your code but not requirements.txt, Docker reuses layers 1-4 from cache. This makes rebuilds fast (seconds instead of minutes).


Multi-Stage Builds

Multi-stage builds separate the "build" phase from the "runtime" phase, producing much smaller images.

# Stage 1: Build
FROM python:3.11 AS builder
WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y gcc

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime (much smaller)
FROM python:3.11-slim
WORKDIR /app

# Copy only installed packages from builder
COPY --from=builder /root/.local /root/.local
COPY . .

# Add local packages to PATH
ENV PATH=/root/.local/bin:$PATH

# Run as non-root user (security)
RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Result: Build stage might be 800MB, but final image is only ~200MB.

Multi-Stage Build with Node (for Frontend + API)

# Stage 1: Build frontend
FROM node:20-alpine AS frontend
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# Stage 2: Python API
FROM python:3.11-slim
WORKDIR /app

COPY api/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY api/ .
# Copy built frontend into static files
COPY --from=frontend /frontend/dist ./static/

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml

For applications with multiple services (web app + database + cache), use docker-compose.

version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src  # Hot reload in development
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Running with Compose

# Build and start all services
docker-compose up --build

# Start in background
docker-compose up -d

# Stop everything
docker-compose down

# View logs
docker-compose logs -f web

# Rebuild a specific service
docker-compose up -d --build web

# Execute command in running container
docker-compose exec web python manage.py migrate

# Scale a service
docker-compose up -d --scale worker=3

Docker Networking

Docker creates a default bridge network for containers. Containers communicate using service names as hostnames.

# docker-compose networking is automatic
services:
  web:
    # Can reach db at "db:5432"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
  db:
    # Can reach web at "web:8000" if needed

# Custom networks for isolation
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

services:
  nginx:
    networks:
      - frontend
  web:
    networks:
      - frontend
      - backend
  db:
    networks:
      - backend

Network Commands

# List networks
docker network ls

# Inspect network
docker network inspect mynetwork

# Create custom network
docker network create mynet

# Connect container to network
docker network connect mynet mycontainer

# Disconnect
docker network disconnect mynet mycontainer

Volume Mounting

Volumes persist data beyond container lifecycle. Without volumes, data is lost when the container stops.

services:
  db:
    image: postgres:15-alpine
    volumes:
      # Named volume (managed by Docker)
      - pgdata:/var/lib/postgresql/data
      # Bind mount (mount host directory)
      - ./init-scripts:/docker-entrypoint-initdb.d

  web:
    build: .
    volumes:
      # Bind mount for development (hot reload)
      - ./src:/app/src
      # Named volume for uploads
      - uploads:/app/uploads

volumes:
  pgdata:
  uploads:

Volume Commands

# List volumes
docker volume ls

# Create volume
docker volume create mydata

# Inspect volume
docker volume inspect mydata

# Run with bind mount
docker run -v /host/path:/container/path myimage

# Run with read-only mount
docker run -v /host/path:/container/path:ro myimage

.dockerignore

Just like .gitignore, .dockerignore tells Docker which files to exclude when building.

Architecture Diagram
__pycache__
*.pyc
*.pyo
.env
.git
.venv
venv/
*.egg-info
dist/
build/
.pytest_cache
.mypy_cache
.dockerignore
Dockerfile
docker-compose.yml
*.md
tests/
.coverage
htmlcov/

Without this, Docker copies unnecessary files into the image, making it larger and potentially leaking secrets.


Essential Docker Commands

# Build an image
docker build -t myapp:latest .

# Build with no cache
docker build --no-cache -t myapp:latest .

# Run a container
docker run -p 8000:8000 myapp:latest

# Run in background
docker run -d -p 8000:8000 --name myapp myapp:latest

# Run with environment variables
docker run -e DEBUG=true -e DATABASE_URL=... myapp:latest

# View running containers
docker ps

# View all containers
docker ps -a

# View logs
docker logs myapp
docker logs -f myapp  # Follow logs

# Execute command in running container
docker exec -it myapp bash

# Stop and remove
docker stop myapp
docker rm myapp

# Force remove
docker rm -f myapp

# Clean up unused images
docker system prune -a

# Check disk usage
docker system df

# View image layers
docker history myapp:latest

Environment Variables and Secrets

import os

# Read from environment (set by Docker)
DATABASE_URL = os.environ["DATABASE_URL"]
API_KEY = os.environ.get("API_KEY")  # Optional
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
# Set defaults in Dockerfile
ENV DEBUG=false
ENV PYTHONUNBUFFERED=1
# Override in docker-compose.yml
environment:
  - DEBUG=true
  - DATABASE_URL=postgresql://...

# Or use .env file (never commit this!)
env_file:
  - .env

Docker Secrets (for Swarm or Compose)

version: '3.8'

services:
  web:
    image: myapp
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    environment: API_KEY

Container Security Best Practices

1. Run as Non-Root User

FROM python:3.11-slim

# Create non-root user
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /home/appuser/app

COPY --chown=appuser:appuser requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

2. Scan for Vulnerabilities

# Scan image with Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image myapp:latest

# Scan with Docker Scout
docker scout cves myapp:latest

3. Use Specific Tags

# BAD — floating tag, unpredictable
FROM python:latest

# GOOD — specific version
FROM python:3.11.7-slim

# BETTER — pin digest for exact reproducibility
FROM python:3.11.7-slim@sha256:abc123...

4. Minimize Installed Packages

# Install only what you need
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# Use slim or alpine base images
FROM python:3.11-slim  # ~150MB
# FROM python:3.11-alpine  # ~50MB (but may need extra setup)

Real-World Example: FastAPI + PostgreSQL

Project Structure

Architecture Diagram
myproject/
+-- app/
|   +-- __init__.py
|   +-- main.py
|   +-- models.py
|   +-- database.py
|   +-- requirements.txt
+-- Dockerfile
+-- docker-compose.yml
+-- .dockerignore
+-- .env

app/main.py

from fastapi import FastAPI
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

DATABASE_URL = os.environ["DATABASE_URL"]

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String, unique=True)

app = FastAPI()

@app.on_event("startup")
def startup():
    Base.metadata.create_all(bind=engine)

@app.get("/users")
def list_users():
    db = SessionLocal()
    users = db.query(User).all()
    return [{"id": u.id, "name": u.name, "email": u.email} for u in users]

@app.post("/users")
def create_user(name: str, email: str):
    db = SessionLocal()
    user = User(name=name, email=email)
    db.add(user)
    db.commit()
    return {"status": "created", "id": user.id}

@app.get("/health")
def health():
    return {"status": "ok"}

Dockerfile

FROM python:3.11-slim

RUN useradd --create-home appuser
WORKDIR /home/appuser/app

COPY app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser app/ .
USER appuser

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

.env

POSTGRES_USER=admin
POSTGRES_PASSWORD=securepassword123
POSTGRES_DB=myapp
DATABASE_URL=postgresql://admin:securepassword123@db:5432/myapp

Common Mistakes

MistakeProblemSolution
Using latest tagUnpredictable buildsPin specific version
Not using .dockerignoreBloated images, leaked secretsAdd .dockerignore
Running as rootSecurity riskCreate non-root user
COPY before requirementsSlow rebuildsCopy requirements first
Not setting PYTHONUNBUFFEREDDelayed log outputSet ENV PYTHONUNBUFFERED=1
Using ADD for simple copiesUnnecessary auto-extractionUse COPY instead
No health checksCan't detect failuresAdd HEALTHCHECK
Storing secrets in imageExposed credentialsUse env vars or Docker secrets

Key Takeaways

  1. Always use slim base images to reduce size
  2. Copy requirements.txt before code for layer caching
  3. Never run containers as root in production
  4. Use multi-stage builds for smaller production images
  5. Use .dockerignore to exclude unnecessary files
  6. Use docker-compose for multi-service applications
  7. Set PYTHONUNBUFFERED=1 for proper log output
  8. Use HEALTHCHECK for container health monitoring
  9. Pin image versions for reproducible builds
  10. Use named volumes for persistent data

Premium Content

Python Docker — Containerized Applications

Unlock this lesson and 900+ advanced tutorials with a Premium plan.

🎯End-to-end Projects
💼Interview Prep
📜Certificates
🤝Community Access

Already a member? Log in

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement