Docker lets you package your app with everything it needs — code, runtime, libraries, system tools — into a single unit called a container. That container runs the same way on your laptop, your coworker’s laptop, and the production server.
The classic “it works on my machine” problem? Docker solves it.
The problem Docker solves
Without Docker:
- You develop on macOS with Node 20, your coworker uses Node 18 on Linux
- Your app needs PostgreSQL 16, but the server has PostgreSQL 14
- You install a library that conflicts with something else on the system
- Deploying means writing a 50-step setup guide and praying
With Docker:
- Everyone runs the exact same environment
- Dependencies are isolated — no conflicts
- Deploying means “run this container”
Containers vs. virtual machines
A virtual machine (VM) runs an entire operating system. A container shares the host OS kernel and only packages the app and its dependencies.
| VM | Container | |
|---|---|---|
| Startup | Minutes | Seconds |
| Size | Gigabytes | Megabytes |
| Overhead | High (full OS) | Low (shared kernel) |
| Isolation | Complete | Process-level |
Containers are lightweight VMs, basically. Fast to start, small to store, easy to move around.
Key concepts
Image — A blueprint. It defines what’s in the container (OS, code, dependencies). You build it once, run it anywhere.
Container — A running instance of an image. You can run multiple containers from the same image.
Dockerfile — A text file with instructions to build an image.
Docker Hub — A registry where people share images (like npm for containers).
Your first container
# Run a container (downloads the image if needed)
docker run hello-world
# Run Nginx web server
docker run -p 8080:80 nginx
# Open http://localhost:8080
# Run a Python shell
docker run -it python:3.12 python
# Run Ubuntu interactively
docker run -it ubuntu bash
Dockerfile example
# Start from Node.js base image
FROM node:20-slim
# Set working directory
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci
# Copy app code
COPY . .
# Build the app
RUN npm run build
# Tell Docker which port the app uses
EXPOSE 3000
# Start the app
CMD ["node", "dist/index.js"]
# Build the image
docker build -t my-app .
# Run it
docker run -p 3000:3000 my-app
Docker Compose — multiple containers
Most apps need more than one service (app + database + cache). Docker Compose lets you define them all in one file:
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
# Start everything
docker compose up
# Stop everything
docker compose down
# Rebuild after code changes
docker compose up --build
Common commands
docker ps # List running containers
docker ps -a # List all containers (including stopped)
docker images # List images
docker stop <container> # Stop a container
docker rm <container> # Remove a container
docker rmi <image> # Remove an image
docker logs <container> # View container logs
docker exec -it <container> bash # Shell into running container
For the full list, see the Docker cheat sheet.
When to use Docker
- Always for production deployments
- Always when working in a team (consistent environments)
- Usually for local development (especially if your app needs databases, Redis, etc.)
- Maybe not for simple scripts or one-off tools
Common issues
If you run into problems, check these:
Next steps
- Install Docker Desktop
- Run
docker run -it ubuntu bashand poke around - Write a Dockerfile for one of your projects
- Add a
docker-compose.ymlwhen you need a database