Docker (Node.js Containerization)
Overview
Docker enables consistent environments for Node.js applications across development, testing, and production. Multi-stage builds reduce image size, docker-compose simplifies local development.
Base Image: node:20-alpine (recommended for small size)
Use case: Deploy TypeScript APIs, ensure consistent environments
Key Benefit: "Works on my machine" → "Works everywhere"
When to Use This Skill
✅ Use Docker when:
-
Deploying to cloud (AWS, GCP, Azure)
-
Need consistent dev environment across team
-
Running CI/CD pipelines
-
Deploying with Kubernetes
-
Need isolated PostgreSQL/Redis for development
❌ Skip Docker when:
-
Simple scripts or CLI tools
-
Serverless deployments (use platform's build)
-
Early prototyping (adds complexity)
Multi-Stage Dockerfile (Production)
Dockerfile
Stage 1: Dependencies
FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force
Stage 2: Build
FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json ./ COPY prisma ./prisma/ COPY src ./src/ RUN npx prisma generate RUN npm run build
Stage 3: Production
FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production
Security: non-root user
RUN addgroup -g 1001 -S nodejs &&
adduser -S nodejs -u 1001 -G nodejs
Copy production artifacts
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist COPY --from=builder --chown=nodejs:nodejs /app/prisma ./prisma COPY --from=builder --chown=nodejs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
USER nodejs EXPOSE 3000
Run migrations then start
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"]
Docker Compose (Development)
docker-compose.yml
version: '3.8'
services: app: build: context: . target: builder # Use builder stage for dev ports: - "3000:3000" environment: NODE_ENV: development DATABASE_URL: postgresql://postgres:postgres@postgres:5432/myapp volumes: - ./src:/app/src:delegated # Hot reload - ./prisma:/app/prisma:delegated depends_on: postgres: condition: service_healthy command: npm run dev
postgres: image: postgres:15-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: myapp ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"] interval: 5s timeout: 5s retries: 10
redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data
volumes: postgres_data: redis_data:
.dockerignore
Dependencies
node_modules
Build outputs
dist .next
Git
.git .gitignore
Environment
.env .env.* !.env.example
IDE
.vscode .idea
Docker
docker-compose* Dockerfile*
Tests & docs
coverage .md **/.test.ts **/*.spec.ts
OS
.DS_Store Thumbs.db
Commands
Development
Start all services
docker-compose up
Start with rebuild
docker-compose up --build
Start in background
docker-compose up -d
View logs
docker-compose logs -f app
Stop all
docker-compose down
Stop and remove volumes (reset DB)
docker-compose down -v
Production Build
Build production image
docker build -t myapp:latest .
Run container
docker run -p 3000:3000
-e DATABASE_URL="postgresql://..."
-e JWT_SECRET="..."
myapp:latest
Run with env file
docker run -p 3000:3000 --env-file .env.production myapp:latest
Debug
Shell into running container
docker-compose exec app sh
Shell into new container
docker run -it myapp:latest sh
View container processes
docker-compose top
Check image size
docker images myapp
Database Migrations in Docker
Development (Auto-migrate)
docker-compose.yml
app: command: sh -c "npx prisma migrate dev && npm run dev"
Production (Deploy migrations)
In Dockerfile CMD
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"]
CI/CD Pipeline
Run migrations in separate step
docker run --rm
-e DATABASE_URL="$PROD_DATABASE_URL"
myapp:latest
npx prisma migrate deploy
Health Checks
Application Health
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
Health Endpoint
// src/routes/health.ts
app.get('/health', async (req, res) => {
try {
await prisma.$queryRawSELECT 1;
res.json({ status: 'healthy', db: 'connected' });
} catch {
res.status(503).json({ status: 'unhealthy', db: 'disconnected' });
}
});
Environment Variables
Development
docker-compose.yml
services: app: environment: NODE_ENV: development DATABASE_URL: postgresql://postgres:postgres@postgres:5432/myapp JWT_SECRET: dev-secret-not-for-production
Production
Pass at runtime
docker run -e DATABASE_URL="..." -e JWT_SECRET="..." myapp
Or use env file
docker run --env-file .env.production myapp
Optimization Tips
Reduce Image Size
Use alpine base
FROM node:20-alpine
Clean npm cache
RUN npm ci && npm cache clean --force
Don't install devDependencies in production
RUN npm ci --only=production
Layer Caching
Copy package files first (changes less often)
COPY package*.json ./ RUN npm ci
Then copy source (changes more often)
COPY . . RUN npm run build
Security
Run as non-root
RUN adduser -S nodejs USER nodejs
Don't expose unnecessary ports
EXPOSE 3000
Use specific versions
FROM node:20.10-alpine
Rules
Do ✅
-
Use multi-stage builds for production
-
Run containers as non-root user
-
Use .dockerignore to exclude unnecessary files
-
Add health checks
-
Pin base image versions
-
Use docker-compose for local development
Avoid ❌
-
Running as root in production
-
Storing secrets in Dockerfile
-
Using latest tag in production
-
Including node_modules in image
-
Skipping .dockerignore
Troubleshooting
"Prisma client not generated": → Add RUN npx prisma generate in builder stage → Copy node_modules/.prisma to runner stage
"Permission denied": → Check file ownership with --chown → Ensure USER matches file owner
"Container exits immediately": → Check logs: docker-compose logs app → Verify CMD is blocking (not backgrounded)
"Can't connect to database": → Use service name as host (postgres, not localhost) → Check depends_on with healthcheck → Verify DATABASE_URL uses container network
"Image too large": → Use alpine base image → Add .dockerignore → Use multi-stage builds → Clean npm cache
File Structure
project/ ├── Dockerfile ├── docker-compose.yml ├── .dockerignore ├── .env.example ├── package.json ├── tsconfig.json ├── prisma/ │ └── schema.prisma └── src/ └── index.ts
References
-
https://docs.docker.com — Official documentation
-
https://docs.docker.com/compose/ — Docker Compose
-
https://hub.docker.com/_/node — Node.js images