Lifecycle Hooks
Lifecycle Hooks
Execute commands at specific points in the process lifecycle for setup, cleanup, and graceful shutdown.
Overview
Lifecycle hooks enable:
- ✅ Database migrations before startup
- ✅ Cache warming and optimization
- ✅ Graceful service termination
- ✅ Cleanup operations on shutdown
- ✅ Service-specific preparation
Hook Types
Global Hooks
Run once for all processes:
hooks:
pre-start:
- name: migrate-database
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
- name: optimize-cache
command: ["php", "artisan", "optimize"]
timeout: 60
post-start:
- name: warmup-cache
command: ["php", "artisan", "cache:warmup"]
timeout: 120
Process-Specific Hooks
Run for individual processes:
processes:
horizon:
command: ["php", "artisan", "horizon"]
shutdown:
pre_stop_hook:
command: ["php", "artisan", "horizon:terminate"]
timeout: 60
Global Pre-Start Hooks
Execute before any processes start.
hooks:
pre-start:
- name: wait-for-database
command: ["./wait-for-db.sh"]
timeout: 60
- name: run-migrations
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
- name: seed-data
command: ["php", "artisan", "db:seed", "--class=ProductionSeeder"]
timeout: 120
Settings (all hook lists):
| Option | Type | Default | Description |
|---|---|---|---|
name |
string | <hook-list>-<index> (e.g. pre-start-0) |
Hook identifier used in logs and metrics |
command |
array | (required) | Command to execute (array format, no shell) |
timeout |
int | 30 |
Maximum execution time in seconds |
retry |
int | 0 |
Retries after a failed attempt |
retry_delay |
int | 0 |
Seconds to wait between retries |
continue_on_error |
bool | false |
Log the failure and continue instead of aborting startup |
env |
map | — | Extra environment variables for the hook command |
working_dir |
string | — | Working directory for the hook command |
Fail-fast vs. best-effort: a failing or timed-out pre-start hook aborts container startup by default — the right behavior for migrations. For best-effort work like cache warming, where a cold cache is degraded but not down, set continue_on_error: true:
hooks:
pre-start:
- name: migrate
command: ["php", "artisan", "migrate", "--force"]
timeout: 300 # failure aborts startup (default)
- name: stache-warm
command: ["php", "please", "stache:warm"]
timeout: 300
continue_on_error: true # log error, start anyway
Use Cases:
- Database migrations: Run schema updates
- Cache warming: Pre-populate caches
- Service dependencies: Wait for external services
- Configuration generation: Create runtime configs
Example: Laravel Optimization
hooks:
pre-start:
# Cache configuration
- name: config-cache
command: ["php", "artisan", "config:cache"]
timeout: 30
# Cache routes
- name: route-cache
command: ["php", "artisan", "route:cache"]
timeout: 30
# Cache views
- name: view-cache
command: ["php", "artisan", "view:cache"]
timeout: 60
# Run migrations
- name: migrate
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
# Create storage link
- name: storage-link
command: ["php", "artisan", "storage:link"]
timeout: 10
Global Post-Start Hooks
Execute after all processes have started and become healthy.
hooks:
post-start:
- name: warmup-cache
command: ["curl", "http://localhost/warmup"]
timeout: 60
- name: notify-deployment
command: ["./notify-slack.sh", "Deployment complete"]
timeout: 10
Use Cases:
- Cache warming: Populate application caches
- Smoke tests: Verify deployment success
- Notifications: Alert teams of successful deployment
- Service registration: Register with service discovery
Process Pre-Stop Hooks
Execute before stopping an individual process.
processes:
horizon:
command: ["php", "artisan", "horizon"]
shutdown:
pre_stop_hook:
command: ["php", "artisan", "horizon:terminate"]
timeout: 60
Use Cases:
- Graceful termination: Signal processes to finish current work
- Job completion: Let workers finish processing
- Connection draining: Close connections gracefully
- State persistence: Save in-progress work
Example: Laravel Horizon Graceful Shutdown
processes:
horizon:
command: ["php", "artisan", "horizon"]
shutdown:
timeout: 120 # Allow time for jobs to finish
pre_stop_hook:
command: ["php", "artisan", "horizon:terminate"]
timeout: 60 # Wait up to 60s for terminate signal
How it works:
- Cbox Init sends
horizon:terminatecommand - Horizon stops accepting new jobs
- Horizon finishes currently running jobs
- Horizon exits gracefully
- If timeout expires, Cbox Init sends SIGTERM
Post-Stop Hooks
There is no per-process post-stop hook. Cleanup that must run after a process
has stopped goes in the global post_stop list, which runs once, after all
processes are down:
hooks:
post-stop:
- name: cleanup
command: ["./cleanup.sh"]
timeout: 30
Use Cases:
- Cleanup: Remove temporary files
- Logging: Record shutdown completion
- Resource release: Free system resources
- Notifications: Alert monitoring systems
Hook Execution Order
Container Start
↓
Global Pre-Start Hooks (sequential)
↓
Process Startup (by priority and depends_on)
↓
Wait for Health Checks
↓
Global Post-Start Hooks (sequential)
↓
... processes running ...
↓
Shutdown Signal (SIGTERM/SIGINT)
↓
Process Pre-Stop Hooks (parallel, per process)
↓
Process Shutdown (reverse priority order)
↓
Global Post-Stop Hooks (sequential)
↓
Container Exit
Defining Hooks via Environment Variables
Global hooks can be defined entirely through environment variables — no YAML mount required. This is the intended path for docker-compose and Kubernetes deployments built on prepared base images:
CBOX_INIT_HOOK_PRE_START_0_NAME=stache-warm
CBOX_INIT_HOOK_PRE_START_0_COMMAND=php,please,stache:warm
CBOX_INIT_HOOK_PRE_START_0_TIMEOUT=300
CBOX_INIT_HOOK_PRE_START_0_ALLOW_FAILURE=true
CBOX_INIT_HOOK_PRE_START_1_COMMAND=php,artisan,event:cache
The pattern is CBOX_INIT_HOOK_<TYPE>_<N>_<FIELD> where <TYPE> is PRE_START, POST_START, PRE_STOP, or POST_STOP and <N> is a non-negative index that determines ordering. Env-defined hooks are appended after any YAML-defined hooks in the same list.
COMMANDaccepts a comma-separated list (php,please,stache:warm) or a JSON array (["php","please","stache:warm"]). Use the JSON form when an argument contains a comma.ALLOW_FAILUREis the env spelling ofcontinue_on_error(both are accepted).- All other hook fields map by name:
NAME,TIMEOUT,RETRY,RETRY_DELAY,WORKING_DIR, andENV_<KEY>for hook environment variables. NAMEdefaults to<hook-list>-<n>(e.g.pre-start-0).
See Environment Variables for the full override reference.
Advanced Patterns
Conditional Execution
#!/bin/bash
# pre-start-hook.sh
# Only run migrations in production
if [ "$APP_ENV" = "production" ]; then
php artisan migrate --force
fi
# Only seed in staging
if [ "$APP_ENV" = "staging" ]; then
php artisan db:seed
fi
hooks:
pre-start:
- name: environment-setup
command: ["./pre-start-hook.sh"]
timeout: 300
Retry Logic
#!/bin/bash
# wait-for-database.sh
MAX_RETRIES=30
RETRY_DELAY=2
for i in $(seq 1 $MAX_RETRIES); do
if php artisan db:ping; then
echo "Database is ready!"
exit 0
fi
echo "Waiting for database... ($i/$MAX_RETRIES)"
sleep $RETRY_DELAY
done
echo "Database not available after $MAX_RETRIES retries"
exit 1
hooks:
pre-start:
- name: wait-for-database
command: ["./wait-for-database.sh"]
timeout: 120 # 30 retries × 2s + buffer
Multi-Step Preparation
hooks:
pre-start:
# Step 1: Wait for dependencies
- name: wait-dependencies
command: ["./wait-for-services.sh"]
timeout: 60
# Step 2: Run migrations
- name: migrate
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
# Step 3: Verify migration
- name: verify-schema
command: ["php", "artisan", "schema:verify"]
timeout: 30
# Step 4: Optimize
- name: optimize
command: ["php", "artisan", "optimize"]
timeout: 60
Environment-Specific Hooks
hooks:
pre-start:
- name: setup
command: ["/setup-${APP_ENV}.sh"] # Uses APP_ENV variable
timeout: 120
setup-production.sh:
#!/bin/bash
set -e
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
setup-staging.sh:
#!/bin/bash
set -e
php artisan migrate:fresh --seed --force
php artisan config:cache
Complete Examples
Laravel Production Setup
version: "1.0"
hooks:
pre-start:
# Optimize Laravel
- name: config-cache
command: ["php", "artisan", "config:cache"]
timeout: 30
- name: route-cache
command: ["php", "artisan", "route:cache"]
timeout: 30
- name: view-cache
command: ["php", "artisan", "view:cache"]
timeout: 60
# Run migrations
- name: migrate
command: ["php", "artisan", "migrate", "--force"]
timeout: 300
# Create storage link
- name: storage-link
command: ["php", "artisan", "storage:link"]
timeout: 10
post-start:
# Warm application cache
- name: cache-warmup
command: ["php", "artisan", "cache:warmup"]
timeout: 120
processes:
php-fpm:
enabled: true
command: ["php-fpm", "-F", "-R"]
nginx:
enabled: true
command: ["nginx", "-g", "daemon off;"]
depends_on: [php-fpm]
horizon:
enabled: true
command: ["php", "artisan", "horizon"]
shutdown:
timeout: 120
pre_stop_hook:
command: ["php", "artisan", "horizon:terminate"]
timeout: 60
Database-Dependent Services
hooks:
pre-start:
# Wait for database
- name: wait-database
command: ["./wait-for-postgres.sh"]
timeout: 60
# Run migrations
- name: migrate
command: ["./run-migrations.sh"]
timeout: 300
# Verify schema
- name: verify
command: ["./verify-schema.sh"]
timeout: 30
wait-for-postgres.sh:
#!/bin/bash
until pg_isready -h localhost -p 5432 -U myuser; do
echo "Waiting for PostgreSQL..."
sleep 2
done
echo "PostgreSQL is ready!"
Troubleshooting
Hook Timeout
Problem: Hook exceeds timeout and container fails to start.
Solution:
hooks:
pre-start:
- name: slow-migration
timeout: 600 # Increase timeout to 10 minutes
Hook Failure
Problem: Hook fails and prevents startup.
Solution: Add error handling to hook script:
#!/bin/bash
# Continue on non-critical errors
php artisan migrate --force || echo "Migration failed, continuing..."
php artisan config:cache || true
exit 0 # Always succeed
Missing Dependencies
Problem: Hook runs before dependencies are available.
Solution: Add wait logic:
#!/bin/bash
# Wait for Redis
timeout 60 bash -c 'until redis-cli ping; do sleep 1; done'
# Then run hook
php artisan cache:clear
Best Practices
✅ Do
- Keep hooks idempotent: Safe to run multiple times
- Add timeouts: Prevent hanging on failures
- Use absolute paths: Avoid working directory issues
- Log output: Make debugging easier
- Handle errors: Graceful degradation
- Test independently: Run hooks in isolation first
❌ Don't
- Don't run daemons: Hooks should exit when done
- Don't assume timing: Other processes may not be ready
- Don't hardcode values: Use environment variables
- Don't ignore exit codes: Failures should fail the hook
- Don't make external dependencies required: Have fallbacks
See Also
- Process Configuration - Process settings
- Health Checks - Health monitoring
- Examples - Real-world hook usage