Docker clicked for me the day I stopped thinking "containers" and started thinking "portable environments."
Here's everything you need to go from zero to productive.
What is Docker, actually?
Docker packages your app + its dependencies into a container — like a lightweight VM, but faster and more portable.
Without Docker: "It works on my machine!"
With Docker: "It works in any machine!"
Install Docker
# macOS / Windows: Install Docker Desktop
# https://www.docker.com/products/docker-desktop/
# Linux (Ubuntu)
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
Your First Container
# Run nginx web server
docker run -p 8080:80 nginx
# Open http://localhost:8080 — it works!
# Ctrl+C to stop
# Run in background
docker run -d -p 8080:80 --name my-nginx nginx
# Check what's running
docker ps
# Stop it
docker stop my-nginx
The Dockerfile
A Dockerfile is a recipe for your container image.
# Node.js app example
FROM node:20-alpine
WORKDIR /app
# Copy package files first (for layer caching)
COPY package*.json ./
RUN npm ci --production
# Copy app code
COPY . .
# Expose port
EXPOSE 3000
# Start command
CMD ["node", "server.js"]
Build and run:
docker build -t my-app .
docker run -p 3000:3000 my-app
Essential Docker Commands
# Images
docker images # List images
docker pull node:20-alpine # Download an image
docker rmi image-name # Remove image
docker build -t name:tag . # Build from Dockerfile
# Containers
docker ps # Running containers
docker ps -a # All containers (including stopped)
docker run -it ubuntu bash # Interactive mode
docker exec -it container bash # Enter running container
docker logs container-name # View logs
docker logs -f container-name # Follow logs live
docker stop container-name # Stop gracefully
docker rm container-name # Remove container
docker rm -f container-name # Force remove
# Volumes (persistent data)
docker run -v /local/path:/container/path image
docker run -v my-volume:/data image
# Networks
docker network ls
docker network create my-net
docker run --network my-net image
# Cleanup
docker system prune # Remove all stopped containers, dangling images
docker system prune -a # Remove everything unused
Docker Compose: Multiple Containers
For apps with multiple services (app + database + cache), use docker-compose.
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
- db
volumes:
- .:/app # Mount code for hot reload in dev
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres-data:
docker compose up # Start everything
docker compose up -d # Detached (background)
docker compose down # Stop everything
docker compose logs -f app # Follow app logs
docker compose exec app bash # Enter app container
Pro Tips
1. Layer caching — copy dependencies first
# ✅ Good: dependencies cached unless package.json changes
COPY package*.json ./
RUN npm ci
COPY . .
# 🚫 Bad: cache busted on every code change
COPY . .
RUN npm ci
2. Use .dockerignore
# .dockerignore
node_modules
.git
.env
*.log
dist
.next
3. Multi-stage builds for tiny images
# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# Production stage — only what's needed
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
# Result: 200MB instead of 1GB
4. Non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
5. Health checks
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1
Common Dockerfile Templates
Python Flask/FastAPI
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Next.js
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["npm", "start"]
Deploy Your Container
Once your image works locally:
# Push to Docker Hub
docker login
docker tag my-app username/my-app:v1.0
docker push username/my-app:v1.0
# Deploy to Railway (easiest)
# Just connect your GitHub repo — Railway auto-detects Dockerfile
# Or to VPS
ssh user@your-server
docker run -d -p 80:3000 --restart unless-stopped username/my-app:v1.0
What made Docker "click" for you? Or what's still confusing? Drop it in the comments.
Top comments (0)