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