Building Docker Compose Configs from Scratch: Engineering Practices for Service Orchestration
Building Docker Compose Configs from Scratch: Engineering Practices for Service Orchestration#
Recently deployed a full-stack project requiring Nginx + Node.js + MySQL + Redis working together. Hand-writing docker-compose.yml revealed how easy it is to mess up port mappings, volume mounts, environment variables, and service dependencies. So I built a generator and revisited Docker Compose core concepts.
Core Technology of YAML Generation#
Docker Compose configuration is essentially YAML-formatted service orchestration. The core data structure:
interface Service {
id: string
image: string // Image name
containerName: string // Container name
ports: PortMapping[] // Port mappings
volumes: VolumeMapping[] // Volume mounts
environment: EnvVar[] // Environment variables
dependsOn: string[] // Service dependencies
restart: string // Restart policy
}
YAML Serialization Pitfalls#
String concatenation for YAML generation seems simple, but has hidden traps:
1. Quote Escaping
Environment variable values may contain special characters ($, #, newlines) that need proper escaping:
function escapeYamlValue(value: string): string {
// Wrap in quotes when containing special characters
if (/[#:{}[\],&*?|<>=!%@`]/.test(value) || value.includes('\n')) {
return `"${value.replace(/"/g, '\\"')}"`
}
return value
}
// Wrong
environment:
- API_KEY=abc#123 // # treated as comment
// Correct
environment:
API_KEY: "abc#123"
2. Port Mapping Formats
Docker Compose supports multiple port formats with different semantics:
# Short syntax (recommended)
ports:
- "3000:3000" # Specify protocol
- "80:80/tcp"
# Long syntax (more control)
ports:
- target: 80
published: 8080
protocol: tcp
mode: host
The generator defaults to short syntax, but watch for IPv6 support:
// Wrong: only listens on IPv4
ports:
- "3000:3000"
// Correct: listens on both IPv4 and IPv6
ports:
- "3000:3000"
expose:
- "3000"
3. Relative Paths in Volume Mounts
// Relative path (relative to docker-compose.yml directory)
volumes:
- ./data:/app/data // Common in development
// Named volume (Docker managed)
volumes:
- mysql-data:/var/lib/mysql
// Anonymous volume (lost when container removed)
volumes:
- /tmp/cache
The generator uses relative paths by default, but databases prefer named volumes:
const volumeConfig = isDatabase
? `${serviceName}-data:/var/lib/mysql` // Named volume
: `./${dir}:/app/${dir}` // Relative path
Service Template System#
To avoid manual configuration each time, I implemented a template system:
const serviceTemplates: Record<string, Partial<Service>> = {
mysql: {
image: 'mysql:8.0',
ports: [{ host: '3306', container: '3306' }],
volumes: [{ host: 'mysql-data', container: '/var/lib/mysql' }],
environment: [
{ key: 'MYSQL_ROOT_PASSWORD', value: 'rootpassword' },
{ key: 'MYSQL_DATABASE', value: 'mydb' }
],
restart: 'unless-stopped'
},
redis: {
image: 'redis:7-alpine',
ports: [{ host: '6379', container: '6379' }],
volumes: [{ host: 'redis-data', container: '/data' }],
restart: 'unless-stopped'
}
}
Template Inheritance and Override#
Users can customize after selecting a template:
function addService(templateKey: string) {
const template = serviceTemplates[templateKey]
const newService: Service = {
id: generateId(),
...template, // Inherit template config
ports: template.ports?.map(p => ({ ...p })) || [], // Deep copy arrays
environment: template.environment?.map(e => ({ ...e })) || []
}
setServices(prev => [...prev, newService])
}
Key point: Arrays must be deep copied, otherwise multiple services share the same array reference - modifying one affects all.
Service Dependencies and Startup Order#
depends_on controls startup order, but there’s a common misconception:
services:
wordpress:
depends_on:
- mysql
This only guarantees MySQL container starts first, not MySQL service is ready. WordPress might try connecting before MySQL initialization completes, causing startup failure.
Health Check Solution#
Docker Compose v2.1+ supports condition: service_healthy:
services:
mysql:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
wordpress:
depends_on:
mysql:
condition: service_healthy
The generator currently uses simple dependencies; health check templates can be added later.
Deep Dive into Restart Policies#
Docker Compose has four restart policies:
| Policy | Behavior | Use Case |
|---|---|---|
no |
Never auto-restart | Development debugging |
always |
Always restart unless manually stopped | Production core services |
on-failure |
Restart only on abnormal exit | Task-based services |
unless-stopped |
Always restart unless manually stopped then Docker restarted | Production recommended |
always vs unless-stopped difference:
# Using always
docker-compose stop
systemctl restart docker # Container auto-starts
# Using unless-stopped
docker-compose stop
systemctl restart docker # Container doesn't start
The generator defaults to unless-stopped, better for production.
Real-World Pitfalls I Encountered#
1. Container Name Conflicts#
services:
app1:
container_name: myapp # Fixed name
app2:
container_name: myapp # Conflict!
Solution: Generator detects duplicate container names and prompts user to modify.
2. Port Conflicts#
ports:
- "80:80" # Host port 80 already used by Nginx
Solution: Generator suggests common alternative ports (3000, 8080, 8000).
3. Environment Variable Leaks#
environment:
- MYSQL_ROOT_PASSWORD=rootpassword # Plaintext password
Solution: Generator prompts users to use .env file:
# .env
MYSQL_ROOT_PASSWORD=your_secure_password
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
4. Volume Permission Issues#
Mounted directories may lack permissions on Linux:
# Create directory and grant permissions on host
mkdir -p ./data/mysql
chown -R 999:999 ./data/mysql # MySQL container UID is 999
Generator Implementation#
Based on these insights, I built an online Docker Compose Generator:
Core Features:
- 9 common service templates (Nginx, MySQL, PostgreSQL, MongoDB, Redis, Node.js, Python, WordPress)
- Visual configuration for ports, volumes, environment variables
- Automatic YAML generation
- Version selection (3.8, 3.7, 3)
Tech Stack:
- React + TypeScript
- State management: useState + useCallback
- YAML generation: String concatenation (avoiding heavy libraries)
Not much code, but covers all Docker Compose core concepts. Hope this helps.
Related: Nginx Config Generator | Environment Variable Editor