Docker Configuration Guide
September 2, 2025 · View on GitHub
This guide provides comprehensive instructions for configuring Docker builds in your project, with special attention to healthcheck requirements for different application types.
Table of Contents
- Understanding the Default Configuration
- Healthcheck Requirements
- Configuration for Different Application Types
- Building and Running
- Container Orchestration
- Troubleshooting
Understanding the Default Configuration
The template includes a production-ready Dockerfile with:
- Multi-stage build: Reduces final image size
- Non-root user: Improves security
- Signal handling: Proper shutdown via dumb-init
- Health check: Default configuration expects a web server
⚠️ Important: Default Healthcheck Assumption
The Dockerfile includes a healthcheck that expects a web server with a /health endpoint on port 3000. If your application:
- ✅ Is a web service: Implement a
/healthendpoint (see examples below) - ❌ Is NOT a web service: Modify or remove the healthcheck (see configuration options)
Healthcheck Requirements
The default healthcheck configuration:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1
This healthcheck:
- Runs every 30 seconds
- Times out after 3 seconds
- Allows 5 seconds for startup
- Retries 3 times before marking unhealthy
- Expects HTTP 200 response from
http://localhost:3000/health
Configuration for Different Application Types
Web Services
For web applications (Express, Fastify, Koa, etc.), implement a health endpoint:
Basic HTTP Server Example
// src/health.example.ts
import http from 'node:http';
import { createChildLogger } from './logger.js';
const logger = createChildLogger('health');
const server = http.createServer((req, res) => {
// Health endpoint
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
return;
}
// Your application routes here
// ...
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
Express Example
// src/server.ts (Express example - Simple version)
import express from 'express';
const app = express();
// Simple health check
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
app.listen(3000);
Or with dependency verification:
// src/server.ts (Express example - With dependency checks)
import express from 'express';
const app = express();
// Health check with dependency verification
app.get('/health', async (req, res) => {
try {
// Check database connection
// await db.ping();
// Check external services
// await redis.ping();
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message,
});
}
});
app.listen(3000);
Fastify Example
// src/server.ts (Fastify example)
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
// Register health route
fastify.get('/health', async (request, reply) => {
return { status: 'OK' };
});
await fastify.listen({ port: 3000, host: '0.0.0.0' });
CLI Tools
For command-line tools that don't run a server, remove the healthcheck:
# In your Dockerfile, replace the HEALTHCHECK line with:
# No healthcheck needed for CLI tools
# HEALTHCHECK NONE
Or remove the healthcheck section entirely:
# Production stage
FROM node:22-alpine
# ... other configuration ...
# Expose port (remove this line for CLI tools)
# EXPOSE 3000
# No healthcheck for CLI applications
# The container will be considered healthy as long as it's running
# Start the application
CMD ["node", "dist/cli.js"]
Worker/Daemon Applications
For long-running workers or daemons, use alternative healthcheck strategies:
Option 1: File-based Healthcheck
# Create a healthcheck that verifies the worker is processing
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD test -f /tmp/worker-healthy || exit 1
In your worker code:
// src/worker.ts
import { writeFileSync, unlinkSync } from 'fs';
// Write health file on successful processing
function markHealthy() {
writeFileSync('/tmp/worker-healthy', Date.now().toString());
}
// Remove health file on errors
function markUnhealthy() {
try {
unlinkSync('/tmp/worker-healthy');
} catch {
// File might not exist
}
}
// Update health status in your worker loop
async function processWork() {
try {
// Do work...
markHealthy();
} catch (error) {
markUnhealthy();
throw error;
}
}
Option 2: Process-based Healthcheck
# Check if the Node.js process is running
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD pgrep -x node || exit 1
Option 3: Custom Script Healthcheck
# Copy a custom health check script
COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD /usr/local/bin/healthcheck.sh
API Services
For API services with external dependencies:
// src/health.ts
import express from 'express';
import { Pool } from 'pg'; // Example: PostgreSQL
import Redis from 'ioredis'; // Example: Redis
const app = express();
const db = new Pool(/* config */);
const redis = new Redis(/* config */);
interface HealthStatus {
status: 'healthy' | 'degraded' | 'unhealthy';
checks: {
[key: string]: {
status: 'up' | 'down';
responseTime?: number;
error?: string;
};
};
timestamp: string;
uptime: number;
}
app.get('/health', async (req, res) => {
const health: HealthStatus = {
status: 'healthy',
checks: {},
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
// Check database
try {
const start = Date.now();
await db.query('SELECT 1');
health.checks.database = {
status: 'up',
responseTime: Date.now() - start,
};
} catch (error) {
health.status = 'unhealthy';
health.checks.database = {
status: 'down',
error: error.message,
};
}
// Check Redis
try {
const start = Date.now();
await redis.ping();
health.checks.redis = {
status: 'up',
responseTime: Date.now() - start,
};
} catch (error) {
health.status = health.status === 'unhealthy' ? 'unhealthy' : 'degraded';
health.checks.redis = {
status: 'down',
error: error.message,
};
}
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
});
Building and Running
Basic Docker Commands
# Build the image
docker build -t my-app .
# Run with default healthcheck (for web services)
docker run -p 3000:3000 my-app
# Run without healthcheck (override at runtime)
docker run --no-healthcheck -p 3000:3000 my-app
# Check health status
docker inspect --format='{{.State.Health.Status}}' <container-id>
Docker Compose Configuration
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
# Override healthcheck if needed
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# Or disable healthcheck
# healthcheck:
# disable: true
Container Orchestration
Kubernetes
For Kubernetes deployments, use native probes instead of Docker healthchecks:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 3000
# Liveness probe (restarts container if unhealthy)
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 30
# Readiness probe (removes from load balancer if not ready)
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
AWS ECS
For ECS task definitions:
{
"family": "my-app",
"containerDefinitions": [
{
"name": "app",
"image": "my-app:latest",
"portMappings": [
{
"containerPort": 3000
}
],
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 3,
"retries": 3,
"startPeriod": 5
}
}
]
}
Troubleshooting
Common Issues
Container Exits Immediately
If your container exits immediately, it might be due to:
- Missing health endpoint: Implement
/healthendpoint if using default config - Wrong port: Ensure your app listens on port 3000 or update the healthcheck
- Startup time: Increase
--start-periodif your app needs more time to start
Container Never Becomes Healthy
Check these common causes:
- Port mismatch: Verify your app listens on the expected port
- Path mismatch: Ensure health endpoint path matches the healthcheck
- Network issues: Container might not be able to reach localhost
Debug with:
# Check container logs
docker logs <container-id>
# Execute healthcheck manually
docker exec <container-id> node -e "require('http').get('http://localhost:3000/health', (r) => {console.log('Status:', r.statusCode)})"
# Inspect health status
docker inspect <container-id> | jq '.[0].State.Health'
Custom Port Configuration
If your app uses a different port:
# Update EXPOSE directive
EXPOSE 8080
# Update healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1
Testing Healthchecks Locally
# Build and run with health status monitoring
docker build -t test-health .
docker run -d --name test-health-container test-health
# Watch health status
watch docker inspect --format='{{.State.Health.Status}}' test-health-container
# View health check logs
docker inspect test-health-container | jq '.[0].State.Health.Log'
# Clean up
docker stop test-health-container
docker rm test-health-container
Best Practices
- Keep health endpoints simple: Avoid expensive operations in health checks
- Use appropriate timeouts: Balance between quick failure detection and false positives
- Consider dependencies: Only check critical dependencies in health endpoints
- Log health checks: Help with debugging but avoid log spam
- Secure health endpoints: Consider authentication for detailed health information
- Version your health API: Include version information in health responses
- Monitor trends: Track health check failures over time