Docker in 10 Minutes

"Works on my machine" used to be a punchline. Docker made it a shipping strategy: pack the machine with the app. This page builds the mental model in the right order — image vs container first, everything else after — plus the three surprises that ambush every beginner: the port error, the vanishing data, and the 40GB of disk you didn't know you were spending.

01The problem: "works on my machine"

Your app doesn't just need your code — it needs a Python version, system libraries, config, environment variables. Deploying used to mean rebuilding that whole pile on another machine and praying. A container packs the app with its entire environment into one sealed, portable box that runs identically anywhere. Same idea as shipping containers: nobody unpacks your furniture at each port.

# the entire pitch, in one command — run redis without installing redis:
docker run redis

# no apt install, no config files, no version conflicts with
# the redis your OTHER project needs. sealed box.
First error you'll meet
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Docker is a client and a background service (the daemon). This error means the service isn't running — on Mac/Windows, Docker Desktop simply isn't open. Start it, wait for the whale icon to settle, retry. Not broken; just asleep.

02Image vs container — the one distinction that matters

Get this one right and Docker stops being confusing. An image is a frozen template — read-only, shareable, versioned. A container is a running instance of an image. Recipe and cake. Class and object. One image can spawn twenty containers, each with its own state, and killing a container never harms the image.

docker pull python:3.12      # download an image (the template)
docker images                # list templates on this machine

docker run -it python:3.12   # bake a cake: image → running container
docker ps                    # list RUNNING containers
docker ps -a                 # ...including stopped ones (they linger!)
The naming decoder
python:3.12 — name, colon, tag (the version). Omitting the tag means :latest, which is the honor-system version: "latest" is whatever the publisher last pushed, and it can silently change under you between pulls. In anything serious, pin the tag — python:3.12, not python.

03docker run: the flags that matter

Ninety percent of docker run is four flags: -d (run in the background), --name (so you can refer to it), -p (wire a port through the box's wall), -e (pass environment variables). The port one deserves a diagram, because everyone reads it backwards at first:

docker run -d --name web -p 8080:80 -e MODE=prod nginx

-p 8080:80
   └──┬─┘└┬┘
   YOUR machine's port : the CONTAINER's port
   "requests to localhost:8080 → into the box at :80"
   mnemonic: host first, box second — outside before inside
You will hit this
Error ... failed to bind host port 0.0.0.0:8080: address already in use  (or port is already allocated)
Something already owns 8080 on your machine — very often a previous copy of the same container still running. docker ps to find it, docker stop it, or just map a different host port: -p 8081:80. The container's inner port never conflicts; only the host side does.

04Look inside a running container

Three commands cover all debugging: logs (what did it print), exec (open a shell inside the box), stop/rm (end it). If a container "doesn't work", the answer is in the logs 95% of the time — containers exit the moment their main process exits, so a crash-on-startup looks like "it just stops".

docker logs web              # everything it printed
docker logs -f web           # follow live (like tail -f)

docker exec -it web sh       # a shell INSIDE the container
  # look around: ls, cat config, ps — then exit

docker stop web              # polite stop
docker rm web                # remove the (stopped) container
The "it immediately exits" mystery
You docker run something, and docker ps shows nothing. It's not gone — it crashed or finished instantly. docker ps -a shows it with an exit code; docker logs <name> shows why. A container lives exactly as long as its main process — it's not a VM idling in the background, it's a process in a box.

05Dockerfile: your own image, as a recipe

A Dockerfile is the recipe that builds an image: start from a base, copy files in, run install commands, declare what starts. Five instructions cover most real Dockerfiles you'll read and write:

FROM python:3.12-slim          # start from this base image
WORKDIR /app                   # cd (and mkdir) inside the image
COPY requirements.txt .        # copy deps list first — see next section!
RUN pip install -r requirements.txt
COPY . .                       # now the rest of the code
CMD ["python", "app.py"]       # what runs when a container starts
docker build -t myapp:1.0 .    # bake it (-t = name:tag, . = here)
docker run -p 8000:8000 myapp:1.0

06Layers & cache: why that COPY order matters

Every Dockerfile line creates a layer, and Docker caches each one: if a line and everything before it didn't change, it's reused instantly. One rule falls out, and it's the single biggest Dockerfile optimization: order lines from least-changing to most-changing. That's why dependencies get copied before code.

# ✗ naive order — change one line of code,
#   and pip reinstalls EVERYTHING (5 min per build):
COPY . .
RUN pip install -r requirements.txt

# ✓ cache-friendly — deps layer only rebuilds when
#   requirements.txt itself changes (build: ~2 sec):
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

This is also why images from the same base share disk space, and why the first build is slow but the second is instant. The cache isn't magic — it's a stack of frozen diffs, invalidated top-down from the first changed line.

07Volumes: where your data actually goes

Here's the surprise that costs people real data: a container's filesystem dies with the container. Run a database in a container, write a month of records, docker rm it — everything is gone. A volume is a persistent disk that lives outside the container and gets mounted in. Databases always get volumes.

# named volume — Docker manages where it lives
docker run -d --name db \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

# bind mount — YOUR folder appears inside the box
# (this is how dev setups live-reload code)
docker run -v ./src:/app/src myapp

docker volume ls             # see your volumes
Say it once, remember forever
Containers are cattle, volumes are the barn. You should be able to delete and recreate any container without losing anything — if that's not true, some state is living inside a container that shouldn't be. The test: docker rm your stack, bring it back up, is everything still there? Run that test before production does.

08Compose: your whole stack in one file

Real apps are several containers — app, database, cache. docker compose reads a YAML file describing all of them and manages the set as one unit. Bonus magic: containers in a compose file reach each other by service name — your app connects to db:5432, not an IP address.

# compose.yaml
services:
  web:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
  db:                          # ← "db" is also its hostname
    image: postgres:16
    volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
  pgdata:
docker compose up -d         # start everything
docker compose logs -f web   # follow one service's logs
docker compose down          # stop and remove (volumes survive)

09Cleanup: the 40GB you didn't know about

Docker never deletes anything on its own. Every pulled image, every stopped container, every orphaned build layer stays on disk — quietly, forever. Six months in, "why is my disk full" has a one-word answer. Two commands manage it:

docker system df             # how much space is Docker eating?

docker system prune          # delete stopped containers + dangling images
docker system prune -a       # ...plus ALL unused images (aggressive)

# prune never touches: running containers, their images, volumes.
# volumes need explicit: docker volume prune (careful — data!)

10Cheat sheet

Keyed by the sentence in your head.

# "run this thing"
docker run -d --name x -p 8080:80 image:tag

# "what's running / what just died?"
docker ps · docker ps -a

# "why is it broken?"
docker logs -f x

# "let me look inside"
docker exec -it x sh

# "port is already allocated"
docker ps → stop the old one, or -p 8081:80

# "my build is slow"
copy deps file → install → THEN copy code

# "where did my data go" (asked once, never again)
volumes for anything that must survive: -v name:/path

# "disk is full"
docker system df → docker system prune

# "start/stop the whole stack"
docker compose up -d · docker compose down

That's working Docker. The concepts run deep (namespaces, cgroups, registries) but this page is what you use weekly. It also pairs naturally with the rest of the stack: containers are Linux processes — the command line applies inside them, and your git-tracked Dockerfile is the whole deploy story.