Skip to content

Web Application Stack Example

This example demonstrates how to orchestrate a complete web application stack using Go Overlay. The stack includes Redis for caching, a database migration service, and a web server with proper dependency management.

Overview

In this example, we'll set up a typical web application stack with the following services:

  • Redis: In-memory cache and session store
  • Database Migration: Runs database schema migrations before the application starts
  • Web Server: The main application server that depends on Redis and completed migrations

This configuration demonstrates:

  • Service dependencies with depends_on
  • Startup delays with wait_after
  • One-time initialization tasks
  • Critical service monitoring with required

Architecture

graph TD
    A[Redis] --> C[Web Server]
    B[DB Migration] --> C
    A -.wait 2s.-> C
    B -.wait 1s.-> C

The web server depends on both Redis and the database migration service. Redis has a 2-second wait period to ensure it's fully ready, while the migration service has a 1-second wait period.

Complete Configuration

Here's the complete services.toml file for this stack:

# Global timeout configurations
[timeouts]
post_script_timeout = 30
service_shutdown_timeout = 10
global_shutdown_timeout = 30
dependency_wait_timeout = 60

# Redis Cache Service
[[services]]
name = "redis"
command = "/usr/bin/redis-server"
args = ["--bind", "0.0.0.0", "--port", "6379", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
enabled = true
required = true

# Database Migration Service (runs once and exits)
[[services]]
name = "db-migration"
command = "/app/migrate"
args = ["-path", "/app/migrations", "-database", "postgres://user:pass@db:5432/myapp?sslmode=disable", "up"]
enabled = true
required = false
depends_on = []

# Web Application Server
[[services]]
name = "web-server"
command = "/app/server"
args = ["--port", "8080", "--redis", "localhost:6379"]
enabled = true
required = true
depends_on = ["redis", "db-migration"]
wait_after = { redis = 2, db-migration = 1 }
user = "appuser"
pre_script = """
#!/bin/bash
echo "Starting web server..."
echo "Checking Redis connection..."
redis-cli -h localhost -p 6379 ping || exit 1
"""
pos_script = """
#!/bin/bash
echo "Web server stopped gracefully"
# Cleanup tasks here
"""

Service Breakdown

1. Redis Service

Role: Provides in-memory caching and session storage for the web application.

Configuration Parameters:

Parameter Value Description
name redis Service identifier
command /usr/bin/redis-server Redis server binary
args Various Bind to all interfaces, set memory limits
enabled true Service starts automatically
required true System shuts down if Redis fails

Why it's required: Redis is critical for application performance and session management. If it fails, the application cannot function properly.

Arguments Explained:

  • --bind 0.0.0.0: Listen on all network interfaces
  • --port 6379: Standard Redis port
  • --maxmemory 256mb: Limit memory usage to 256MB
  • --maxmemory-policy allkeys-lru: Evict least recently used keys when memory limit is reached

2. Database Migration Service

Role: Executes database schema migrations before the web server starts. This is a one-time task that runs and exits.

Configuration Parameters:

Parameter Value Description
name db-migration Service identifier
command /app/migrate Migration tool binary
args Migration parameters Path to migrations and database URL
enabled true Runs on startup
required false System continues if migrations fail (use with caution)
depends_on [] No dependencies

Why it's not required: Depending on your use case, you might want the system to continue even if migrations fail (e.g., if migrations are already applied). However, in production, you might want to set this to true.

Arguments Explained:

  • -path /app/migrations: Directory containing migration files
  • -database postgres://...: PostgreSQL connection string
  • up: Apply all pending migrations

Migration Best Practices

  • Always test migrations in a staging environment first
  • Consider making this service required = true in production
  • Use migration tools that support rollback (like golang-migrate, Flyway, or Liquibase)
  • Keep migrations idempotent when possible

3. Web Server Service

Role: The main application server that handles HTTP requests, uses Redis for caching, and requires completed database migrations.

Configuration Parameters:

Parameter Value Description
name web-server Service identifier
command /app/server Application binary
args Server parameters Port and Redis connection
enabled true Starts automatically
required true Critical service
depends_on ["redis", "db-migration"] Waits for dependencies
wait_after { redis = 2, db-migration = 1 } Additional wait times
user appuser Runs as non-root user
pre_script Health checks Validates Redis before starting
pos_script Cleanup Runs after service stops

Why it's required: This is the main application service. If it fails, the entire system should shut down.

Dependency Chain:

  1. Redis starts first (no dependencies)
  2. DB Migration starts (no dependencies, can run in parallel with Redis)
  3. After both complete, Go Overlay waits:
  4. 2 seconds after Redis is running
  5. 1 second after DB Migration completes
  6. Web Server starts

Pre-script Explained:

The pre-script validates that Redis is accessible before starting the web server:

#!/bin/bash
echo "Starting web server..."
echo "Checking Redis connection..."
redis-cli -h localhost -p 6379 ping || exit 1

If Redis doesn't respond to PING, the script exits with code 1, preventing the web server from starting.

Post-script Explained:

The post-script runs after the web server stops, allowing for cleanup tasks:

#!/bin/bash
echo "Web server stopped gracefully"
# Cleanup tasks here

You could add tasks like:

  • Flushing remaining cache entries
  • Closing database connections
  • Sending shutdown notifications

Startup Sequence

Here's what happens when you start this stack:

  1. T+0s: Go Overlay starts
  2. T+0s: Redis and DB Migration start in parallel (no dependencies)
  3. T+2s: Redis is running and healthy
  4. T+3s: DB Migration completes
  5. T+4s: Wait period for Redis (2s) completes
  6. T+4s: Wait period for DB Migration (1s) completes
  7. T+4s: Pre-script runs and validates Redis connection
  8. T+4s: Web Server starts

Running the Stack

Using Go Overlay Directly

# Start in daemon mode
go-overlay daemon --config /services.toml

# Check status
go-overlay status

# View logs
journalctl -u go-overlay -f

Expected Output

[INFO] Starting service: redis
[INFO] Starting service: db-migration
[INFO] Service redis is running (PID: 1234)
[INFO] Service db-migration completed successfully
[INFO] Waiting 2s after redis startup
[INFO] Waiting 1s after db-migration completion
[INFO] Running pre-script for web-server
[INFO] Starting service: web-server
[INFO] Service web-server is running (PID: 1235)
[INFO] All services started successfully

Customization Options

Adding More Services

You can extend this stack with additional services:

# Background Worker
[[services]]
name = "worker"
command = "/app/worker"
args = ["--queue", "default", "--redis", "localhost:6379"]
enabled = true
required = false
depends_on = ["redis", "web-server"]
wait_after = { redis = 2 }
user = "appuser"

# Nginx Reverse Proxy
[[services]]
name = "nginx"
command = "/usr/sbin/nginx"
args = ["-g", "daemon off;"]
enabled = true
required = true
depends_on = ["web-server"]
wait_after = { web-server = 3 }

Adjusting Wait Times

Modify wait_after values based on your services' startup times:

# For slower services
wait_after = { redis = 5, db-migration = 2 }

# For faster services
wait_after = { redis = 1, db-migration = 0 }

Environment-Specific Configuration

Use environment variables in your commands:

[[services]]
name = "web-server"
command = "/app/server"
args = [
    "--port", "${PORT:-8080}",
    "--redis", "${REDIS_URL:-localhost:6379}",
    "--env", "${APP_ENV:-production}"
]

Troubleshooting

Web Server Won't Start

Symptom: Web server fails to start or crashes immediately.

Possible Causes:

  1. Redis is not accessible
  2. Check: redis-cli -h localhost -p 6379 ping
  3. Solution: Increase wait_after time for Redis

  4. Database migrations failed

  5. Check migration logs
  6. Solution: Fix migration errors or set required = false temporarily

  7. Pre-script validation fails

  8. Check pre-script output in logs
  9. Solution: Adjust validation logic or increase wait times

Services Start in Wrong Order

Symptom: Web server starts before dependencies are ready.

Solution: Verify depends_on and wait_after configuration:

depends_on = ["redis", "db-migration"]  # Must list all dependencies
wait_after = { redis = 2, db-migration = 1 }  # Adjust timing

Redis Connection Refused

Symptom: Pre-script fails with "Connection refused".

Possible Causes:

  1. Redis hasn't finished initializing
  2. Solution: Increase wait_after.redis value

  3. Redis is binding to wrong interface

  4. Solution: Check Redis --bind argument

  5. Port conflict

  6. Solution: Change Redis port or check for conflicts

Best Practices

  1. Always use required = true for critical services: If your application can't function without a service, mark it as required.

  2. Set appropriate wait times: Don't use arbitrary values. Test your services to determine actual startup times.

  3. Use pre-scripts for validation: Validate dependencies before starting services to fail fast.

  4. Run services as non-root users: Use the user parameter for security.

  5. Keep migrations idempotent: Ensure migrations can be run multiple times safely.

  6. Monitor service health: Use the go-overlay status command to check service states.

  7. Test dependency chains: Verify that services start in the correct order during development.

  8. Use meaningful service names: Choose names that clearly indicate the service's purpose.

Next Steps