Docker
Docker is a platform for building, shipping, and running applications in containers. Created in 2013, Docker revolutionized software deployment by packaging applications with their dependencies into portable containers that run consistently anywhere. For JavaScript/TypeScript developers, Docker ensures "it works on my machine" becomes "it works everywhere."
What is Docker?
Docker packages apps into containers:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# Build image
docker build -t myapp .
# Run container
docker run -p 3000:3000 myapp
Key Concepts:
- Image: Blueprint for container (like a class)
- Container: Running instance of image (like an object)
- Dockerfile: Instructions to build image
- Registry: Storage for images (Docker Hub, GitHub Container Registry)
- Volume: Persistent storage for containers
- Network: Communication between containers
Why Docker?
1. Consistent Environments
Developer's Machine:
- Node.js 18
- macOS
- npm 9
Production Server:
- Node.js 20
- Linux
- npm 10
Result: "Works on my machine" ❌
With Docker:
Developer + Production:
- Same Docker image
- Same Node version
- Same dependencies
- Same OS
Result: Works everywhere ✓
2. Isolation
Each container runs in isolation:
Host Machine
├── Container 1 (Node.js 18)
├── Container 2 (Node.js 20)
├── Container 3 (Python 3.11)
└── Container 4 (Go 1.21)
No conflicts between apps.
3. Portability
Build once → Run anywhere
Developer laptop ✓
CI/CD server ✓
Production server ✓
Cloud (AWS, GCP, Azure) ✓
Kubernetes ✓
4. Easy Rollback
# Deploy v2
docker run myapp:v2
# Issue found? Rollback to v1
docker stop myapp-v2
docker run myapp:v1
Instant rollbacks.
Dockerfile Basics
Simple Node.js App
# Use official Node.js image
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy app files
COPY . .
# Expose port
EXPOSE 3000
# Start app
CMD ["node", "server.js"]
Multi-Stage Build (Smaller Images)
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
Result: Smaller final image (no build tools).
TypeScript App
FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source
COPY . .
# Build TypeScript
RUN npm run build
# Remove dev dependencies
RUN npm prune --production
EXPOSE 3000
CMD ["node", "dist/index.js"]
Next.js App
FROM node:20-alpine AS base
# Dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
Docker Commands
Build and Run
# Build image
docker build -t myapp .
# Build with tag
docker build -t myapp:v1.0.0 .
# Run container
docker run -p 3000:3000 myapp
# Run in background (detached)
docker run -d -p 3000:3000 --name myapp-container myapp
# Run with environment variables
docker run -p 3000:3000 -e DATABASE_URL=postgresql://... myapp
# Run with volume (persistent data)
docker run -p 3000:3000 -v $(pwd)/data:/app/data myapp
Managing Containers
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop container
docker stop myapp-container
# Start stopped container
docker start myapp-container
# Remove container
docker rm myapp-container
# View logs
docker logs myapp-container
# Follow logs (tail -f)
docker logs -f myapp-container
# Execute command in running container
docker exec -it myapp-container sh
# Inspect container
docker inspect myapp-container
Managing Images
# List images
docker images
# Remove image
docker rmi myapp
# Pull image from registry
docker pull node:20-alpine
# Push image to registry
docker push myusername/myapp:v1.0.0
# Tag image
docker tag myapp myusername/myapp:v1.0.0
# Remove unused images
docker image prune
# Remove all unused images, containers, volumes
docker system prune -a
Docker Compose
Run multi-container applications:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
# Start all services
docker-compose up
# Start in background
docker-compose up -d
# Stop all services
docker-compose down
# View logs
docker-compose logs -f
# Rebuild and start
docker-compose up --build
# Stop and remove volumes
docker-compose down -v
Environment Variables
.env File
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/db
API_KEY=secret123
NODE_ENV=production
# docker-compose.yml
services:
app:
build: .
env_file:
- .env
ports:
- "3000:3000"
In Dockerfile
# Set default environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Use build arguments
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
# Pass build args
docker build --build-arg NODE_VERSION=18 -t myapp .
Volumes (Persistent Data)
Named Volumes
# Create volume
docker volume create myapp-data
# Use volume
docker run -v myapp-data:/app/data myapp
# List volumes
docker volume ls
# Remove volume
docker volume rm myapp-data
Bind Mounts (Local Development)
# Mount local directory
docker run -v $(pwd):/app myapp
# Read-only mount
docker run -v $(pwd):/app:ro myapp
# docker-compose.yml
services:
app:
build: .
volumes:
- .:/app # Bind mount current directory
- /app/node_modules # Don't override node_modules
Networking
Default Bridge Network
# Containers can communicate by name in docker-compose
# app can reach db at: postgresql://db:5432
Custom Networks
# Create network
docker network create myapp-network
# Run containers on network
docker run --network myapp-network --name app myapp
docker run --network myapp-network --name db postgres
# Inspect network
docker network inspect myapp-network
Docker for Development
Hot Reload with Volumes
# docker-compose.dev.yml
version: '3.8'
services:
app:
build:
context: .
target: development
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev
# Dockerfile with dev stage
FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
# Run dev environment
docker-compose -f docker-compose.dev.yml up
Best Practices
1. Use .dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.DS_Store
dist
coverage
.next
2. Multi-Stage Builds
# ✓ Good - multi-stage (smaller image)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
CMD ["node", "dist/server.js"]
# ❌ Bad - single stage (larger image with dev dependencies)
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
3. Layer Caching
# ✓ Good - copy package.json first (caches dependencies)
COPY package*.json ./
RUN npm ci
COPY . .
# ❌ Bad - copy everything first (reinstalls deps on any file change)
COPY . .
RUN npm ci
4. Use Alpine Images
# ✓ Good - alpine (smaller)
FROM node:20-alpine # ~40 MB
# ❌ Bad - full image (larger)
FROM node:20 # ~300 MB
5. Run as Non-Root User
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
CMD ["node", "server.js"]
6. Health Checks
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
// healthcheck.js
const http = require('http');
const options = {
host: 'localhost',
port: 3000,
path: '/health',
timeout: 2000,
};
const request = http.request(options, (res) => {
process.exit(res.statusCode === 200 ? 0 : 1);
});
request.on('error', () => process.exit(1));
request.end();
Security Best Practices
1. Scan Images for Vulnerabilities
# Scan image
docker scan myapp
# Or use Trivy
trivy image myapp
2. Don't Store Secrets in Images
# ✓ Good - pass as environment variable
docker run -e API_KEY=$API_KEY myapp
# ❌ Bad - hardcoded in Dockerfile
ENV API_KEY=secret123
3. Use Official Images
# ✓ Good - official image
FROM node:20-alpine
# ❌ Bad - unknown source
FROM randomuser/node
CI/CD Integration
GitHub Actions
# .github/workflows/docker.yml
name: Docker Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t myapp .
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub
run: |
docker tag myapp ${{ secrets.DOCKER_USERNAME }}/myapp:latest
docker push ${{ secrets.DOCKER_USERNAME }}/myapp:latest
Docker vs. Alternatives
Feature | Docker | VM | Bare Metal |
---|---|---|---|
Startup Time | Seconds | Minutes | N/A |
Resource Usage | Low | High | Lowest |
Isolation | Process-level | OS-level | None |
Portability | High | Medium | Low |
Size | MB | GB | N/A |
Best For | Apps | Multiple OSes | Maximum performance |
Key Takeaways
- Containers package app + dependencies
- Consistent environments (dev = prod)
- Portable (run anywhere)
- Use multi-stage builds for smaller images
- Use .dockerignore to exclude files
- Docker Compose for multi-container apps
- Layer caching speeds up builds
- Alpine images are smaller
- Best for consistent deployments, microservices
Related Topics
- Kubernetes - Container orchestration
- Cloud Run - Serverless containers
- ECS - AWS container service
- Deployment Overview - Compare deployment strategies
- Node.js - Popular runtime for Docker
Docker is the foundation of modern application deployment. It ensures your app runs consistently everywhere, from development to production. Master Docker to unlock containerization, microservices, and cloud-native deployment strategies.