Docker

6 min read #DevOps #Django #Docker

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:

  • FROM picks a base image with Python already installed
  • Copying requirements.txt before the rest of the code means that changing a template doesn't force a full reinstall of your dependencies
  • CMD is the process the container runs, here the Gunicorn application server
  • Replace config.wsgi with 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

  1. 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.

  2. Painless onboarding โ€” A new contributor installs Docker, runs one command, and has a working app, with no Python version juggling.

  3. Clean isolation โ€” Two apps needing different Python or library versions can live on the same server without touching each other.

  4. 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

  1. Install Docker. On a desktop, use Docker Desktop. On a Linux server, follow the official instructions for your distribution.

  2. Check that it works:

    bash docker run --rm hello-world

  3. Write a Dockerfile for a small project, starting from the example above.

  4. Build and run it:

    bash docker build -t blog . docker run --rm -p 8000:8000 --env-file .env blog

  5. 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.

Read more about the tech stack