Docker solved the 'works on my machine' problem. Learn how containers work, why they matter, and how to use them effectively.
Before Docker, deploying software was painful:
Developer: "It works on my machine!"
Ops: "Well, we're not shipping your machine."
The issue: different environments have different OS versions, library versions, configurations. Code that works locally breaks in production.
Docker's solution: package your application and all its dependencies into a container that runs identically everywhere.
A container is a lightweight, isolated process that includes everything needed to run an application:
Containers share the host OS kernel but are isolated from each other. This makes them much lighter than virtual machines.
| Virtual Machine | Container | |
|---|---|---|
| Size | GBs | MBs |
| Startup time | Minutes | Seconds |
| OS | Full OS per VM | Shared host kernel |
| Isolation | Strong | Good |
| Overhead | High | Low |
A read-only template for creating containers. Like a class in OOP.
A running instance of an image. Like an object in OOP.
Instructions for building an image.
Where images are stored. Docker Hub is the public registry.
# Start from an official base image
FROM node:18-alpine
# Set working directory inside container
WORKDIR /app
# Copy dependency files first (layer caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose the port
EXPOSE 3000
# Command to run when container starts
CMD ["node", "server.js"]Docker builds images in layers. Each instruction is a layer. If a layer hasn't changed, Docker reuses the cached version.
# ✅ Good — dependencies cached separately from code
COPY package*.json ./
RUN npm ci
COPY . .
# ❌ Bad — any code change invalidates npm ci cache
COPY . .
RUN npm ci# Build an image
docker build -t my-app:1.0 .
# Run a container
docker run -p 3000:3000 my-app:1.0
# Run in background (detached)
docker run -d -p 3000:3000 --name my-app my-app:1.0
# List running containers
docker ps
# View logs
docker logs my-app
# Execute command inside container
docker exec -it my-app sh
# Stop and remove
docker stop my-app && docker rm my-app
# List images
docker images
# Remove image
docker rmi my-app:1.0Real applications need multiple services — app server, database, cache. Docker Compose orchestrates them.
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
postgres_data# Start all services
docker compose up -d
# View logs for all services
docker compose logs -f
# Stop all services
docker compose down# Stage 1 — Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2 — Production (only what's needed)
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]Result: production image is 200MB instead of 1GB — only the built output, no dev tools.