Node.js / Express API
Production Dockerfile
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first (better caching)
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
# Security: non-root user
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --production && npm cache clean --force
COPY --from=builder --chown=app:app /app/dist ./dist
USER app
ENV NODE_ENV=production
EXPOSE 4000
# Use dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
Health Check Endpoint
// server.js — Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
app.get('/ready', async (req, res) => {
try {
// Check database connection
await db.query('SELECT 1');
// Check Redis connection
await redis.ping();
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
Docker Compose with Health Checks
# docker-compose.yml
services:
api:
build: .
ports:
- "4000:4000"
environment:
DATABASE_URL: postgres://admin:secret@db:5432/myapp
REDIS_URL: redis://redis:6379
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
command: redis-server --appendonly yes
volumes:
pgdata:
redis-data:
Graceful Shutdown
// Graceful shutdown for Docker containers
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Handle SIGTERM (docker stop)
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
server.close(async () => {
// Close database connections
await db.end();
await redis.quit();
console.log('Server closed.');
process.exit(0);
});
// Force exit after 30 seconds
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
});
Logging Best Practices
// Log to stdout/stderr (Docker captures these)
// ✅ Good: structured JSON logging
const log = (level, message, meta = {}) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
...meta
}));
};
log('info', 'Server started', { port: 4000 });
log('error', 'Database connection failed', { error: err.message });
// ❌ Bad: logging to files inside the container
// fs.writeFileSync('/var/log/app.log', message);
Production Checklist
- ✅ Use multi-stage builds to minimize image size
- ✅ Run as non-root user
- ✅ Implement health check endpoints (
/healthand/ready) - ✅ Handle SIGTERM for graceful shutdown
- ✅ Log to stdout/stderr in structured JSON format
- ✅ Use
dumb-initortinias PID 1