Every developer has heard it — or said it: “But it works on my machine!” Different operating systems, library versions and settings make software behave differently from one computer to the next.
Docker solves this by packaging your app together with everything it needs into a container that runs the same way everywhere: your laptop, a teammate’s PC or a cloud server.
Containers vs virtual machines
| Virtual machine | Container | |
|---|---|---|
| Includes | A full operating system | Just your app and its dependencies |
| Size | Gigabytes | Often tens of megabytes |
| Start-up time | Minutes | Seconds or less |
| Isolation | Very strong | Strong (shares the host kernel) |
Containers are lightweight because they share the host’s operating system kernel instead of booting their own.
Key terms in 30 seconds
- Image — a read-only template (like a recipe) containing your app and its environment
- Container — a running instance of an image (the meal cooked from the recipe)
- Dockerfile — a text file with the instructions to build an image
- Registry — a place to store and share images, such as Docker Hub
Step 1: Install Docker
Download Docker Desktop for Windows or macOS from docker.com, or install Docker Engine on Linux. Then check it works:
docker --version
docker run hello-world
If you see a “Hello from Docker!” message, you’re ready.
Step 2: A tiny app to containerize
Create a folder with a simple Node.js server, server.js:
const http = require("http");
http.createServer((req, res) => {
res.end("Hello from inside a container! 🐳");
}).listen(3000, () => console.log("Listening on port 3000"));
And a minimal package.json:
{
"name": "hello-docker",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" }
}
Step 3: Write the Dockerfile
# Start from an official, small Node.js image
FROM node:22-alpine
# Work inside /app in the container
WORKDIR /app
# Copy dependency files first (better caching)
COPY package*.json ./
RUN npm install --omit=dev
# Copy the rest of the code
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Copying package.json before the rest of your code means Docker can reuse the cached npm install layer when only your code changes. Builds become dramatically faster.
Step 4: Build and run
docker build -t hello-docker .
docker run -p 3000:3000 hello-docker
Open http://localhost:3000 and you’ll see your message. The -p 3000:3000 flag maps port 3000 on your computer to port 3000 inside the container.
Useful commands to know:
docker ps # list running containers
docker stop <id> # stop a container
docker images # list images
docker logs <id> # see a container's output
Step 5: Multi-container apps with Docker Compose
Real apps need more than one service — say, a web app and a database. Docker Compose describes them all in one file, compose.yaml:
services:
web:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Start everything with a single command:
docker compose up
Notice the web app reaches the database at the hostname db — Compose creates a private network where services find each other by name. The db-data volume keeps your data safe when containers restart.
Never commit real passwords in compose.yaml. Use an .env file (added to .gitignore) or your platform’s secrets manager.
Where to go next
- Add a .dockerignore file to keep
node_modulesand.gitout of your image - Learn multi-stage builds to make production images even smaller
- Push your image to a registry and deploy it to a cloud service
Containers are now the standard way to ship software. Once you’ve containerized one app, you’ll want to containerize everything.
The Dockerfile layer caching tip cut my build time from 3 minutes to 10 seconds. Thank you!