Introduction
"It works on my machine" is one of the oldest jokes in software, and one of the most expensive. Your laptop has one Python version, your server has another, and a teammate has a third. A system library is missing here, a different one is installed there, and the bug only shows up in production. Docker exists to make that whole category of problem go away.
The Problem Docker Solves
Deploying a web app traditionally means recreating its environment by hand on every machine:
- Installing the right Python version and system packages
- Installing dependencies without conflicting with other apps on the same server
- Remembering which environment variables and config files the app expects
- Repeating all of it, slightly differently, for development, staging, and production
Virtual machines solve part of this by bundling a whole operating system with your app, but they're heavy. They take gigabytes of disk, minutes to boot, and a lot of memory for what is often one small service.
The Docker Approach
Docker packages your application and everything it needs into a container: a lightweight, isolated process that carries its own filesystem, dependencies, and configuration. Containers share the host's kernel instead of running a full operating system, so they start in seconds and use a fraction of the resources.
Core idea: Describe your environment once, in a file. Then build it and run it identically anywhere Docker is installed.
Three Terms to Know
- Image โ A read-only template built from your instructions. Think of it as the recipe plus all the ingredients, frozen.
- Container โ A running instance of an image. You can start many containers from one image.
- Dockerfile โ The text file that describes how to build an image.
Simple Example
# Run a throwaway Python container and print its version
docker run --rm python:3.13-slim python --version
That one command downloads an image (the first time only), starts a container, runs the command, prints the result, and cleans up after itself. You never installed Python on your machine.
Packaging a Django Blog
Here is a minimal Dockerfile for a Django project like this one:
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Install dependencies first so Docker can cache this layer
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Then copy the application code
COPY . .
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
What's happening:
FROMpicks a base image with Python already installed- Copying
requirements.txtbefore the rest of the code means that changing a template doesn't force a full reinstall of your dependencies CMDis the process the container runs, here the Gunicorn application server- Replace
config.wsgiwith your project's actual WSGI module path
Add a .dockerignore file so you don't copy junk into the image:
.git
.venv
__pycache__/
*.pyc
db.sqlite3
.env
Keeping .env and your local database out of the image matters: images get pushed to registries and shared, and secrets baked into them are hard to take back.
Docker Compose
Real apps usually involve more than one command. Docker Compose describes your whole setup in a single compose.yaml:
services:
web:
build: .
ports:
- "8000:8000"
env_file: .env
volumes:
- db-data:/app/data
restart: unless-stopped
volumes:
db-data:
If your blog uses SQLite, point the database path into that mounted folder (for example /app/data/db.sqlite3, read from an environment variable in your settings). The volume is what keeps your data alive when the container is replaced.
Start everything with:
docker compose up -d --build
Why This Matters
-
Reproducible environments โ The image that passed your tests is byte-for-byte the one that runs in production. Environment drift stops being a source of mystery bugs.
-
Painless onboarding โ A new contributor installs Docker, runs one command, and has a working app, with no Python version juggling.
-
Clean isolation โ Two apps needing different Python or library versions can live on the same server without touching each other.
-
Easy rollbacks โ Deploying means starting a new image. If it's broken, start the previous one. Nothing on the host was modified.
Key Docker Commands
| Command | Purpose | Example |
|---|---|---|
docker build |
Build an image from a Dockerfile | docker build -t blog . |
docker run |
Start a container from an image | docker run -p 8000:8000 blog |
docker ps |
List running containers | docker ps -a |
docker logs |
See a container's output | docker logs -f <name> |
docker exec |
Run a command inside a container | docker exec -it <name> sh |
docker compose up |
Start every service in compose.yaml |
docker compose up -d |
Real-World Use Cases
Beyond shipping a web app, Docker is handy for:
- Local services โ Run PostgreSQL, Redis, or a mail catcher without installing anything permanently
- CI pipelines โ Run tests in a clean, identical environment every time
- Trying software safely โ Spin up a tool, poke at it, and throw it away
- Multi-service apps โ Web server, database, and background worker described together in one file
- Consistent tooling โ Pin the exact version of a build tool for the whole team
Getting Started
-
Install Docker. On a desktop, use Docker Desktop. On a Linux server, follow the official instructions for your distribution.
-
Check that it works:
bash docker run --rm hello-world -
Write a Dockerfile for a small project, starting from the example above.
-
Build and run it:
bash docker build -t blog . docker run --rm -p 8000:8000 --env-file .env blog -
Graduate to Compose once you have more than one service or need persistent volumes.
Know the Limits
Docker is not magic, and a few things trip people up:
- Containers are disposable. Anything written inside a container's filesystem disappears when it's removed. Use volumes for data you want to keep.
- It adds a layer to learn. Networking, volumes, and image sizes all have their own quirks.
- It isn't a security boundary by default. Don't run as root inside the container when you can avoid it, and keep base images updated.
- It isn't always necessary. For a single small site on one server, a plain virtualenv and a process manager can be perfectly fine.
The Philosophy
Docker isn't about adopting the newest infrastructure fashion. It's about making the environment part of your code instead of folklore. When your Dockerfile lives in the repository next to your application, "how do I run this?" always has one answer, and it's written down.
Start with one Dockerfile for one app. Add Compose when you need a second service. Reach for anything bigger only when you can name the problem it solves.
This post is part of a series on the tools behind this blog: Django, HTMX, Tailwind CSS, PostgreSQL, SQLite, and now Docker. Posts are authored in the Django admin as Markdown and rendered to HTML on save.