'But it works on MY machine!' โ said every developer ever. Docker fixes this by shipping the entire machine. It's like putting your app in a shipping container.
Containers are lightweight VMs. Your app + all its dependencies = one package. No more 'works on my machine.' No more crying.
# Docker architecture:
# Dockerfile โ docker build โ Image โ docker run โ Container
# Check installation
docker --version
docker info
# Run a test container
docker run hello-world
# Key concepts:
# Image = blueprint (read-only template)
# Container = running instance of an image
# Dockerfile = recipe to build an image
# Registry = where images are stored (Docker Hub)
# Volume = persistent storage
# Network = how containers talk to each other
A Dockerfile is a cooking recipe for computers. 'Take Python, add your code, expose port 3000, and serve.' If only real cooking were this easy.
# Dockerfile (for a Node.js app):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# Dockerfile (for a Python app):
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
# Best practices:
# 1. Use specific tags (not 'latest')
# 2. Multi-stage builds to reduce size
# 3. Layer caching: COPY package.json BEFORE source code
# 4. Use .dockerignore like .gitignore
docker build creates images. docker run starts containers. docker ps shows running containers. It's a whole ecosystem in your terminal.
# Build image
docker build -t my-app:1.0 .
# Run container
docker run -d --name my-app -p 3000:3000 my-app:1.0
# -d = detached (background)
# -p = port mapping (host:container)
# --name = give it a name
# List containers
docker ps # running
docker ps -a # all (including stopped)
# Container lifecycle
docker stop my-app
docker start my-app
docker restart my-app
docker rm my-app # remove stopped container
# Logs & exec
docker logs my-app
docker logs -f my-app # follow (like tail -f)
docker exec -it my-app sh # shell inside container
# Clean up
docker system prune -f # delete everything not running
Your app has a database, a cache, and a queue. Starting them separately is annoying. Docker Compose runs ALL of them with 'docker compose up'. Magic.
# docker-compose.yml
yaml
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
- db
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: pass
volumes:
pgdata:
# Run: docker compose up -d
# Stop: docker compose down
# Logs: docker compose logs -f
# Rebuild: docker compose up --build
Containers are ephemeral (poof, gone). Volumes keep data. Networks let containers talk. Without these, your container is a lonely island.
# Volumes (persistent data)
docker volume create my-data
docker run -v my-data:/app/data my-app
# Bind mount (share host directory)
docker run -v /host/path:/container/path my-app
# Networks
docker network create my-net
docker run --network my-net --name app app-image
docker run --network my-net --name db postgres
# Now 'db' hostname resolves to the database container!
# Environment variables
docker run -e DATABASE_URL=postgres://... -e DEBUG=true my-app
# Resource limits
docker run --memory="512m" --cpus="0.5" my-app
# Health checks in Dockerfile:
# HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1