Docker Fundamentals
Build small, reproducible Python images and run multi-service stacks with Docker Compose.
By the end, you’ll be able to…
- Explain how an image, container, and layer relate.
- Write a Dockerfile for a Python service.
- Keep images small and free of secrets.
- Describe a local stack with Docker Compose.
01An image is a reproducible filesystem
A Docker image is an immutable filesystem plus metadata. A container is a running process using that image. Each Dockerfile instruction creates a layer, and layers are cached. Put the least frequently changing instructions first so a source edit does not reinstall every dependency.
A reproducible image starts from a pinned base, copies only the files it needs, and runs one clear command. The goal is not merely 'it works on my machine'; it is that the same inputs produce the same runnable artifact on another machine.
FROM python:3.12-slimENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1WORKDIR /appCOPY requirements.txt .RUN python -m pip install --no-cache-dir -r requirements.txtCOPY app.py .RUN useradd --create-home appuserUSER appuserEXPOSE 8000CMD ["python", "app.py"]02Build and run a service
docker build creates an image from the Dockerfile and a context. The context is the directory sent to the daemon, so a large .dockerignore can dramatically reduce build time. Tag images with a version or commit SHA, not only latest, so a deployment can identify exactly what it is running.
Publish ports only when the service must be reachable from the host. Inside a Compose network, services reach each other by service name and internal port. Environment variables carry configuration; secrets should be mounted or injected by the platform, not baked into the image.
docker build -t task-api:1.0.0 .docker run --rm -p 8000:8000 --env-file .env task-api:1.0.0docker psdocker logs <container>03Keep images small and safe
Use a slim base, install only runtime dependencies, and remove build caches. A multi-stage build can compile wheels in one stage and copy the installed environment into a smaller runtime stage. Do not copy the entire repository when a few files are enough.
Run as a non-root user, pin base image versions, and scan the image for known vulnerabilities as part of CI. Never put a token in an ARG or ENV instruction: layers are inspectable. A .dockerignore should exclude .git, tests, local virtual environments, and secrets. A smaller image also reduces the attack surface and the time every deploy spends pulling layers.
.git.venv__pycache__.pytest_cache.envtests*.md04Compose a local stack
Docker Compose describes multiple services, their build or image, environment, ports, and volumes. It is ideal for a web service plus a database. Named volumes preserve database files across container recreation; bind mounts are convenient for local source during development.
Add a healthcheck so dependent services wait for readiness, not merely for process start. depends_on controls start order but does not prove the database accepts connections. A healthcheck plus an application retry policy is a more honest readiness strategy.
services: api: build: . ports: - "8000:8000" environment: DATABASE_URL: postgresql://app:secret@db:5432/app depends_on: db: condition: service_healthy db: image: postgres:16 environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: app volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U app"] interval: 5s retries: 10volumes: db_data:The ideas worth keeping.
- Images are built from cached layers; order instructions for reuse.
- Run as a non-root user and keep secrets out of layers.
- Compose describes relationships, volumes, and readiness checks.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Dockerize a standard-library service
Write a Dockerfile for a Python service with no external dependencies. Run as a non-root user, expose port 8000, and set unbuffered output.
Explore a worked solution
FROM python:3.12-slimENV PYTHONUNBUFFERED=1WORKDIR /appCOPY app.py .RUN useradd --create-home appuserUSER appuserEXPOSE 8000CMD ["python", "app.py"]No pip install is needed for a standard-library service. The non-root user limits the damage if the process is compromised, and unbuffered output makes container logs timely.
Compose a web service and database
Write a compose file with an api service built from the current directory and a postgres service with a named volume and a healthcheck. Make the api wait for the database to become healthy.
Explore a worked solution
services: api: build: . ports: ["8000:8000"] depends_on: db: condition: service_healthy db: image: postgres:16 environment: POSTGRES_PASSWORD: secret volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 10volumes: db_data:The healthcheck makes readiness observable. A named volume keeps the database alive when the container is recreated; an anonymous volume would be harder to identify and back up.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Dockerize a standard-library service
Write a Dockerfile for a Python service with no external dependencies. Run as a non-root user, expose port 8000, and set unbuffered output.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.