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 FOREGROUNDprevents 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:
runcommand keeps Caddy in foreground- Runs as dedicated
caddyuser 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:
--nodaemonizekeeps PHP-FPM in foreground- Configuration validation before startup
- Runs as
www-datauser (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
postgresuser - 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:
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¶
-
Always run services as non-root users when possible
-
Use pre-scripts for validation to fail fast
-
Use post-scripts for cleanup to ensure graceful shutdown
-
Set appropriate wait times based on actual startup duration
-
Mark critical services as required
-
Use meaningful service names
-
Document complex configurations with comments
-
Test dependency chains to ensure correct startup order
-
Keep scripts simple and focused on single responsibilities
-
Log important events in pre/post scripts for debugging
Next Steps¶
- Review Web Application Stack for a complete multi-service example
- Learn about Docker Integration for containerized deployments
- Explore Dependency Management for advanced scenarios
- Read about Service Lifecycle to understand service states