The Problem It Solves
You've written some code. It works perfectly on your laptop. You hand it off to a teammate, and it crashes immediately. They have a different version of Python, a different OS, a missing library. Sound familiar? This is the classic "it works on my machine" problem — and it's been plaguing software teams for decades.
Docker exists to kill that problem dead.
So What Is Docker?
Docker is a tool that lets you package your application together with everything it needs to run — the code, the runtime, the libraries, the configuration — into a single portable unit called a container. You ship the container, not just the code. Wherever that container runs, the app behaves identically.
Think of it like this: instead of mailing someone a recipe and hoping they have the right ingredients, you mail them the fully prepped meal in a sealed box. They just heat it up. Same result, every time.
Containers vs. Virtual Machines
This is the most important concept to get right early, because Docker often gets confused with virtual machines (VMs). They both isolate software, but in fundamentally different ways.
| Virtual Machine | Docker Container | |
|---|---|---|
| What it virtualizes | The entire hardware | Just the OS process layer |
| Includes | Full guest OS (Windows, Linux, etc.) | Only the app + its dependencies |
| Size | Gigabytes | Megabytes |
| Startup time | Minutes | Seconds (often milliseconds) |
| Resource usage | Heavy | Very lightweight |
| Isolation level | Full OS isolation | Process-level isolation |
A VM says: "I'll pretend to be an entirely separate computer." A container says: "I'll just wall off my little corner of this computer."
Both share the physical hardware, but containers also share the host machine's OS kernel rather than booting their own OS. This is why they're so much lighter and faster.
The Core Building Blocks
Image
A Docker image is the blueprint. It's a read-only, layered snapshot of everything your app needs — OS base, dependencies, your code, start-up commands. Images don't run; they're the instructions for building something that does.
You can think of an image like a class in object-oriented programming. It defines the structure, but isn't a live instance yet.
Container
A container is a running instance of an image. Start the same image ten times, you get ten isolated containers. This is the object to the image's class.
Containers are:
- Isolated — they can't see each other's internals by default
- Ephemeral — when you stop and remove a container, anything written inside it is gone (unless you use volumes)
- Portable — runs the same on your Mac, a Linux server, or a cloud VM
Dockerfile
A Dockerfile is a plain text file where you write instructions to build your image. It's the recipe. Here's a minimal real example:
FROM python:3.11-slim # Start from an official Python base image
WORKDIR /app # Set the working directory inside the container
COPY requirements.txt . # Copy your dependency list
RUN pip install -r requirements.txt # Install dependencies
COPY . . # Copy the rest of your code
CMD ["python", "app.py"] # The command to run when the container starts
Each line in a Dockerfile creates a new layer in the image. Docker is smart about caching these layers — if you only change your app code, it won't reinstall dependencies from scratch. It reuses the cached layers and only rebuilds what changed.
Docker Registry
Once you build an image, you need somewhere to store it so others (or other servers) can pull it. That's a registry. Docker Hub is the public default — it's like GitHub but for images. Private registries exist too (AWS ECR, Google Container Registry, etc.).
The workflow looks like:
Write Dockerfile → docker build → Image → docker push → Registry ↓ docker pull → Container runs anywhere
Essential Commands
You don't need to memorize every flag, but these are the ones you'll use constantly:
# Build an image from a Dockerfile in the current directory
docker build -t my-app:1.0 .
# Run a container from an image
docker run my-app:1.0
# Run in the background (detached mode)
docker run -d my-app:1.0
# Map a port: host_port:container_port
# (so your browser can reach the app running inside the container)
docker run -p 8080:3000 my-app:1.0
# List running containers
docker ps
# List all containers (including stopped ones)
docker ps -a
# Stop a running container
docker stop <container_id>
# Remove a container
docker rm <container_id>
# List all local images
docker images
# Remove an image
docker rmi my-app:1.0
# Pull an image from Docker Hub
docker pull nginx
# Open a shell inside a running container (extremely useful for debugging)
docker exec -it <container_id> bash
Volumes — Persisting Data
Containers are ephemeral by design. If you write data inside a container and it stops, that data vanishes. Volumes are how you break that rule intentionally.
A volume is a link between a folder on your host machine and a folder inside the container. Changes in one are reflected in the other.
# Mount a host folder into the container
docker run -v /my/local/folder:/app/data my-app:1.0
This is essential for things like databases running in containers — you don't want your data wiped every restart.
Docker Compose — Running Multiple Containers Together
Real apps rarely have just one piece. You might have a web server, a database, and a caching layer. Spinning each up manually with docker run gets painful fast.
Docker Compose lets you define your entire multi-container setup in a single docker-compose.yml file:
version: "3"
services:
web:
build: .
ports:
- "8080:3000"
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Then you just run:
docker compose up # Start everything
docker compose down # Stop and clean up
One command. Everything starts together, wired up, ready to go.
How It All Fits Together
Here's the full mental model end-to-end:
- You write your app code
- You write a
Dockerfiledescribing how to package it docker buildreads the Dockerfile and produces an imagedocker runtakes that image and spins up a live container- You
docker pushthe image to a registry - Anyone, anywhere pulls that image and runs the exact same container
The same container you tested on your laptop ships to staging, then to production, without a single "but it worked for me" conversation.
Why This Matters in Practice
- Consistency — Dev, staging, and production all run identical environments
- Speed — Onboarding a new developer takes minutes instead of a day of environment setup
- Isolation — Multiple apps with conflicting dependencies live side by side without issues
- Scalability — Containers are lightweight enough to spin up dozens of instances on demand
- Foundation for Kubernetes — Once you understand Docker, Kubernetes (the system for orchestrating thousands of containers) becomes conceptually approachable