Skip to content

Common Patterns

This guide provides reusable configuration patterns for common service types and scenarios. Use these patterns as templates for your own services.

Web Servers

Nginx

Nginx is commonly used as a reverse proxy, static file server, or load balancer.

[[services]]
name = "nginx"
command = "/usr/sbin/nginx"
args = ["-g", "daemon off;"]
enabled = true
required = true
pre_script = """
#!/bin/bash
# Test nginx configuration before starting
nginx -t || exit 1
echo "Nginx configuration is valid"
"""
pos_script = """
#!/bin/bash
echo "Nginx stopped, cleaning up..."
# Remove PID file if it exists
rm -f /var/run/nginx.pid
"""

Key Points:

  • -g "daemon off;" keeps Nginx in foreground mode (required for Go Overlay)
  • Pre-script validates configuration before starting
  • Post-script cleans up PID files

Apache HTTP Server

[[services]]
name = "apache"
command = "/usr/sbin/apache2"
args = ["-D", "FOREGROUND"]
enabled = true
required = true
pre_script = """
#!/bin/bash
# Validate Apache configuration
apache2ctl configtest || exit 1
echo "Apache configuration is valid"
"""

Key Points:

  • -D FOREGROUND prevents Apache from daemonizing
  • Configuration validation prevents startup with invalid config

Caddy

Caddy is a modern web server with automatic HTTPS.

[[services]]
name = "caddy"
command = "/usr/bin/caddy"
args = ["run", "--config", "/etc/caddy/Caddyfile"]
enabled = true
required = true
user = "caddy"

Key Points:

  • run command keeps Caddy in foreground
  • Runs as dedicated caddy user for security
  • Automatic HTTPS certificate management

Application Servers

Node.js Application

[[services]]
name = "node-app"
command = "/usr/bin/node"
args = ["/app/server.js"]
enabled = true
required = true
user = "appuser"
depends_on = ["redis", "postgres"]
wait_after = { redis = 2, postgres = 3 }
pre_script = """
#!/bin/bash
echo "Starting Node.js application..."
# Check if required environment variables are set
if [ -z "$DATABASE_URL" ]; then
    echo "ERROR: DATABASE_URL not set"
    exit 1
fi
# Verify database connectivity
node /app/scripts/check-db.js || exit 1
"""

Key Points:

  • Waits for dependencies (Redis, PostgreSQL)
  • Validates environment variables before starting
  • Checks database connectivity in pre-script

Python/Gunicorn Application

[[services]]
name = "gunicorn"
command = "/usr/local/bin/gunicorn"
args = [
    "--bind", "0.0.0.0:8000",
    "--workers", "4",
    "--worker-class", "sync",
    "--timeout", "60",
    "--access-logfile", "-",
    "--error-logfile", "-",
    "app:app"
]
enabled = true
required = true
user = "appuser"
depends_on = ["redis"]
wait_after = { redis = 2 }

Key Points:

  • Multiple workers for concurrent request handling
  • Logs to stdout/stderr for container compatibility
  • Configurable timeout for long-running requests

PHP-FPM

[[services]]
name = "php-fpm"
command = "/usr/sbin/php-fpm"
args = ["--nodaemonize", "--fpm-config", "/etc/php/8.2/fpm/php-fpm.conf"]
enabled = true
required = true
user = "www-data"
pre_script = """
#!/bin/bash
# Test PHP-FPM configuration
php-fpm --test --fpm-config /etc/php/8.2/fpm/php-fpm.conf || exit 1
"""

Key Points:

  • --nodaemonize keeps PHP-FPM in foreground
  • Configuration validation before startup
  • Runs as www-data user (standard for web services)

Databases

PostgreSQL

[[services]]
name = "postgres"
command = "/usr/lib/postgresql/15/bin/postgres"
args = [
    "-D", "/var/lib/postgresql/15/main",
    "-c", "config_file=/etc/postgresql/15/main/postgresql.conf"
]
enabled = true
required = true
user = "postgres"
pre_script = """
#!/bin/bash
# Ensure data directory exists and has correct permissions
if [ ! -d "/var/lib/postgresql/15/main" ]; then
    echo "ERROR: PostgreSQL data directory not found"
    exit 1
fi
# Check if database is initialized
if [ ! -f "/var/lib/postgresql/15/main/PG_VERSION" ]; then
    echo "Initializing PostgreSQL database..."
    /usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/15/main
fi
"""
pos_script = """
#!/bin/bash
echo "PostgreSQL stopped gracefully"
# Perform any cleanup if needed
"""

Key Points:

  • Runs as postgres user
  • Pre-script initializes database if needed
  • Specifies data directory and config file

MySQL/MariaDB

[[services]]
name = "mariadb"
command = "/usr/sbin/mariadbd"
args = [
    "--user=mysql",
    "--datadir=/var/lib/mysql",
    "--socket=/var/run/mysqld/mysqld.sock"
]
enabled = true
required = true
pre_script = """
#!/bin/bash
# Ensure data directory exists
mkdir -p /var/lib/mysql /var/run/mysqld
chown -R mysql:mysql /var/lib/mysql /var/run/mysqld

# Initialize database if needed
if [ ! -d "/var/lib/mysql/mysql" ]; then
    echo "Initializing MariaDB database..."
    mysql_install_db --user=mysql --datadir=/var/lib/mysql
fi
"""

Key Points:

  • Creates necessary directories
  • Initializes database on first run
  • Sets proper ownership

Redis

[[services]]
name = "redis"
command = "/usr/bin/redis-server"
args = [
    "--bind", "127.0.0.1",
    "--port", "6379",
    "--maxmemory", "256mb",
    "--maxmemory-policy", "allkeys-lru",
    "--save", "900", "1",
    "--save", "300", "10",
    "--save", "60", "10000",
    "--dir", "/var/lib/redis"
]
enabled = true
required = true
user = "redis"
pre_script = """
#!/bin/bash
# Ensure Redis data directory exists
mkdir -p /var/lib/redis
chown redis:redis /var/lib/redis
"""

Key Points:

  • Binds to localhost for security
  • Configures memory limits and eviction policy
  • Enables persistence with save intervals
  • Creates data directory in pre-script

MongoDB

[[services]]
name = "mongodb"
command = "/usr/bin/mongod"
args = [
    "--dbpath", "/var/lib/mongodb",
    "--bind_ip", "127.0.0.1",
    "--port", "27017",
    "--logpath", "/var/log/mongodb/mongod.log"
]
enabled = true
required = true
user = "mongodb"
pre_script = """
#!/bin/bash
# Create necessary directories
mkdir -p /var/lib/mongodb /var/log/mongodb
chown -R mongodb:mongodb /var/lib/mongodb /var/log/mongodb
"""

Background Workers

Celery Worker

[[services]]
name = "celery-worker"
command = "/usr/local/bin/celery"
args = [
    "-A", "myapp.celery",
    "worker",
    "--loglevel=info",
    "--concurrency=4",
    "--max-tasks-per-child=1000"
]
enabled = true
required = false
user = "appuser"
depends_on = ["redis"]
wait_after = { redis = 2 }
pre_script = """
#!/bin/bash
echo "Starting Celery worker..."
# Verify Redis connection
python -c "import redis; r = redis.Redis(host='localhost', port=6379); r.ping()" || exit 1
"""

Key Points:

  • Not marked as required (workers can be restarted independently)
  • Depends on Redis message broker
  • Limits tasks per child to prevent memory leaks
  • Validates Redis connection before starting

Celery Beat Scheduler

[[services]]
name = "celery-beat"
command = "/usr/local/bin/celery"
args = [
    "-A", "myapp.celery",
    "beat",
    "--loglevel=info",
    "--schedule=/var/lib/celery/celerybeat-schedule"
]
enabled = true
required = false
user = "appuser"
depends_on = ["redis", "celery-worker"]
wait_after = { redis = 2, celery-worker = 1 }
pre_script = """
#!/bin/bash
# Ensure schedule directory exists
mkdir -p /var/lib/celery
chown appuser:appuser /var/lib/celery
"""

Key Points:

  • Depends on both Redis and worker
  • Creates schedule directory
  • Runs as application user

Sidekiq (Ruby)

[[services]]
name = "sidekiq"
command = "/usr/local/bin/bundle"
args = ["exec", "sidekiq", "-C", "/app/config/sidekiq.yml"]
enabled = true
required = false
user = "appuser"
depends_on = ["redis"]
wait_after = { redis = 2 }

User Switching for Security

Running services as non-root users is a critical security practice.

Creating Users in Pre-script

[[services]]
name = "web-app"
command = "/app/server"
args = ["--port", "8080"]
enabled = true
required = true
user = "appuser"
pre_script = """
#!/bin/bash
# Create user if it doesn't exist
if ! id -u appuser > /dev/null 2>&1; then
    useradd -r -s /bin/false -d /app appuser
    echo "Created user: appuser"
fi

# Set ownership of application directory
chown -R appuser:appuser /app

# Ensure log directory exists with correct permissions
mkdir -p /var/log/app
chown appuser:appuser /var/log/app
"""

Key Points:

  • Creates system user (-r) without login shell (-s /bin/false)
  • Sets ownership of application files
  • Creates log directory with proper permissions

Different Users for Different Services

# Web server runs as www-data
[[services]]
name = "nginx"
command = "/usr/sbin/nginx"
args = ["-g", "daemon off;"]
enabled = true
required = true
user = "www-data"

# Application runs as appuser
[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
user = "appuser"
depends_on = ["nginx"]

# Database runs as postgres
[[services]]
name = "postgres"
command = "/usr/lib/postgresql/15/bin/postgres"
args = ["-D", "/var/lib/postgresql/15/main"]
enabled = true
required = true
user = "postgres"

Key Points:

  • Each service runs with minimal required privileges
  • Limits blast radius if a service is compromised
  • Follows principle of least privilege

Pre-script and Post-script Patterns

Environment Validation

[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
pre_script = """
#!/bin/bash
# Validate required environment variables
required_vars=("DATABASE_URL" "REDIS_URL" "SECRET_KEY" "API_KEY")

for var in "${required_vars[@]}"; do
    if [ -z "${!var}" ]; then
        echo "ERROR: Required environment variable $var is not set"
        exit 1
    fi
done

echo "All required environment variables are set"
"""

Database Migration

[[services]]
name = "web-app"
command = "/app/server"
enabled = true
required = true
depends_on = ["postgres"]
wait_after = { postgres = 3 }
pre_script = """
#!/bin/bash
echo "Running database migrations..."

# Wait for database to be ready
until pg_isready -h localhost -p 5432 -U myuser; do
    echo "Waiting for PostgreSQL..."
    sleep 2
done

# Run migrations
/app/migrate -path /app/migrations -database "$DATABASE_URL" up

if [ $? -eq 0 ]; then
    echo "Migrations completed successfully"
else
    echo "ERROR: Migrations failed"
    exit 1
fi
"""

Log Rotation Setup

[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
pre_script = """
#!/bin/bash
# Setup log rotation
cat > /etc/logrotate.d/app <<EOF
/var/log/app/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0640 appuser appuser
}
EOF

echo "Log rotation configured"
"""

Cleanup on Shutdown

[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
pos_script = """
#!/bin/bash
echo "Application shutting down..."

# Close database connections gracefully
/app/scripts/close-connections.sh

# Archive logs
tar -czf /var/log/app/shutdown-$(date +%Y%m%d-%H%M%S).tar.gz /var/log/app/*.log

# Send shutdown notification
curl -X POST https://monitoring.example.com/api/shutdown \
    -H "Content-Type: application/json" \
    -d '{"service": "app", "timestamp": "'$(date -Iseconds)'"}'

echo "Cleanup completed"
"""

Health Check Before Start

[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
depends_on = ["redis", "postgres"]
wait_after = { redis = 2, postgres = 3 }
pre_script = """
#!/bin/bash
echo "Performing health checks..."

# Check Redis
if ! redis-cli -h localhost -p 6379 ping > /dev/null 2>&1; then
    echo "ERROR: Redis is not responding"
    exit 1
fi
echo "✓ Redis is healthy"

# Check PostgreSQL
if ! pg_isready -h localhost -p 5432 -U myuser > /dev/null 2>&1; then
    echo "ERROR: PostgreSQL is not ready"
    exit 1
fi
echo "✓ PostgreSQL is healthy"

# Check disk space
available=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$available" -gt 90 ]; then
    echo "WARNING: Disk usage is above 90%"
fi
echo "✓ Disk space check completed"

echo "All health checks passed"
"""

Service Dependencies

Linear Dependency Chain

Services start in sequence: A → B → C

[[services]]
name = "database"
command = "/usr/bin/postgres"
enabled = true
required = true

[[services]]
name = "cache"
command = "/usr/bin/redis-server"
enabled = true
required = true
depends_on = ["database"]
wait_after = { database = 2 }

[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
depends_on = ["cache"]
wait_after = { cache = 2 }

Parallel Dependencies

Service C depends on both A and B, which start in parallel:

[[services]]
name = "redis"
command = "/usr/bin/redis-server"
enabled = true
required = true

[[services]]
name = "postgres"
command = "/usr/bin/postgres"
enabled = true
required = true

[[services]]
name = "app"
command = "/app/server"
enabled = true
required = true
depends_on = ["redis", "postgres"]
wait_after = { redis = 2, postgres = 3 }

Complex Dependency Graph

Multiple services with various dependencies:

# Foundation services (no dependencies)
[[services]]
name = "redis"
command = "/usr/bin/redis-server"
enabled = true
required = true

[[services]]
name = "postgres"
command = "/usr/bin/postgres"
enabled = true
required = true

# Migration depends on database
[[services]]
name = "migration"
command = "/app/migrate"
enabled = true
required = false
depends_on = ["postgres"]
wait_after = { postgres = 3 }

# API depends on database and cache
[[services]]
name = "api"
command = "/app/api"
enabled = true
required = true
depends_on = ["redis", "postgres", "migration"]
wait_after = { redis = 2, postgres = 3, migration = 1 }

# Worker depends on cache and API
[[services]]
name = "worker"
command = "/app/worker"
enabled = true
required = false
depends_on = ["redis", "api"]
wait_after = { redis = 2, api = 5 }

# Web server depends on API
[[services]]
name = "nginx"
command = "/usr/sbin/nginx"
args = ["-g", "daemon off;"]
enabled = true
required = true
depends_on = ["api"]
wait_after = { api = 3 }

Conditional Service Enablement

Environment-Based Enablement

Use environment variables to control which services run:

[[services]]
name = "debug-proxy"
command = "/usr/bin/mitmproxy"
enabled = false  # Set to true via environment variable
required = false

Then enable via environment:

# In Dockerfile or docker-compose.yml
ENV ENABLE_DEBUG_PROXY=true

Development vs Production

# Development-only service
[[services]]
name = "hot-reload"
command = "/usr/bin/nodemon"
args = ["--watch", "/app", "/app/server.js"]
enabled = false  # Enable only in development
required = false

# Production-only service
[[services]]
name = "monitoring-agent"
command = "/usr/bin/datadog-agent"
enabled = true  # Disable in development
required = false

Best Practices Summary

  1. Always run services as non-root users when possible

    user = "appuser"
    

  2. Use pre-scripts for validation to fail fast

    pre_script = """
    #!/bin/bash
    # Validate configuration
    myapp --validate-config || exit 1
    """
    

  3. Use post-scripts for cleanup to ensure graceful shutdown

    pos_script = """
    #!/bin/bash
    # Cleanup temporary files
    rm -rf /tmp/myapp-*
    """
    

  4. Set appropriate wait times based on actual startup duration

    wait_after = { database = 5, cache = 2 }
    

  5. Mark critical services as required

    required = true  # System shuts down if this service fails
    

  6. Use meaningful service names

    name = "api-server"  # Good
    name = "service1"    # Avoid
    

  7. Document complex configurations with comments

    # This service handles background job processing
    # It requires Redis for job queue management
    [[services]]
    name = "worker"
    # ...
    

  8. Test dependency chains to ensure correct startup order

  9. Keep scripts simple and focused on single responsibilities

  10. Log important events in pre/post scripts for debugging

Next Steps