Skip to content

Custom Initialization

Custom Initialization

Cbox provides hooks for running custom code at container startup. Use these for database migrations, waiting for services, cache warming, and dynamic configuration.

Initialization Methods

Method 1: Init Scripts Directory

Place executable scripts in /docker-entrypoint-init.d/:

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

COPY scripts/init-*.sh /docker-entrypoint-init.d/
RUN chmod +x /docker-entrypoint-init.d/*.sh

Execution order: Scripts run in version-sort order (sort -V), so 2-foo.sh runs before 10-bar.sh - numeric prefixes behave the way you expect without zero-padding. A *.sh file without the executable bit is logged as a warning and skipped (never silently ignored).

/docker-entrypoint-init.d/
  01-wait-for-db.sh      # Runs first
  02-run-migrations.sh   # Runs second
  03-cache-warmup.sh     # Runs third

Method 2: Environment Variables

Use built-in Laravel features:

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    environment:
      LARAVEL_MIGRATE_ENABLED: "true"    # Run migrations
      LARAVEL_OPTIMIZE_ENABLED: "true"   # Cache config/routes

Method 3: Custom Entrypoint

For complete control:

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

COPY custom-entrypoint.sh /usr/local/bin/custom-entrypoint.sh
RUN chmod +x /usr/local/bin/custom-entrypoint.sh

ENTRYPOINT ["/usr/local/bin/custom-entrypoint.sh"]
#!/bin/bash
# custom-entrypoint.sh

# Your custom initialization
echo "Running custom initialization..."
# ... your code ...

# Call original entrypoint
exec /usr/local/bin/docker-entrypoint.sh "$@"

Common Patterns

Wait for Database

#!/bin/bash
# /docker-entrypoint-init.d/01-wait-for-db.sh

set -e

MAX_RETRIES=30
RETRY_INTERVAL=2

echo "Waiting for database..."

for i in $(seq 1 $MAX_RETRIES); do
    if php artisan db:monitor --database=mysql 2>/dev/null; then
        echo "Database is ready!"
        exit 0
    fi
    echo "Attempt $i/$MAX_RETRIES: Database not ready, waiting ${RETRY_INTERVAL}s..."
    sleep $RETRY_INTERVAL
done

echo "ERROR: Database not available after $MAX_RETRIES attempts"
exit 1

Wait for Redis

#!/bin/bash
# /docker-entrypoint-init.d/01-wait-for-redis.sh

set -e

REDIS_HOST="${REDIS_HOST:-redis}"
REDIS_PORT="${REDIS_PORT:-6379}"
MAX_RETRIES=30

echo "Waiting for Redis at $REDIS_HOST:$REDIS_PORT..."

for i in $(seq 1 $MAX_RETRIES); do
    if nc -z "$REDIS_HOST" "$REDIS_PORT" 2>/dev/null; then
        echo "Redis is ready!"
        exit 0
    fi
    echo "Attempt $i/$MAX_RETRIES: Redis not ready..."
    sleep 2
done

echo "ERROR: Redis not available"
exit 1

Run Migrations

#!/bin/bash
# /docker-entrypoint-init.d/02-run-migrations.sh

set -e

if [ -f "/var/www/html/artisan" ]; then
    echo "Running database migrations..."

    if [ "${APP_ENV:-production}" = "production" ]; then
        php artisan migrate --force --no-interaction
    else
        php artisan migrate --no-interaction
    fi

    echo "Migrations completed!"
fi

Cache Warmup (Laravel)

#!/bin/bash
# /docker-entrypoint-init.d/03-cache-warmup.sh

set -e

if [ -f "/var/www/html/artisan" ]; then
    echo "Warming up caches..."

    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan event:cache

    echo "Cache warmup completed!"
fi

Generate App Key (If Missing)

#!/bin/bash
# /docker-entrypoint-init.d/00-generate-key.sh

set -e

if [ -f "/var/www/html/artisan" ]; then
    # Check if APP_KEY is set
    if [ -z "${APP_KEY}" ] || [ "${APP_KEY}" = "base64:" ]; then
        echo "WARNING: APP_KEY not set, generating..."
        php artisan key:generate --force
    fi
fi

Fix Permissions

#!/bin/bash
# /docker-entrypoint-init.d/00-fix-permissions.sh

set -e

WORKDIR="${WORKDIR:-/var/www/html}"

echo "Fixing permissions..."

# Laravel directories
for dir in storage bootstrap/cache; do
    if [ -d "$WORKDIR/$dir" ]; then
        chown -R www-data:www-data "$WORKDIR/$dir"
        chmod -R 775 "$WORKDIR/$dir"
    fi
done

echo "Permissions fixed!"

Dynamic Configuration

#!/bin/bash
# /docker-entrypoint-init.d/01-dynamic-config.sh

set -e

# Generate config from environment
cat > /var/www/html/config/dynamic.php << EOF
<?php
return [
    'api_url' => '${API_URL}',
    'feature_flags' => [
        'new_feature' => ${FEATURE_NEW:-false},
    ],
];
EOF

echo "Dynamic configuration generated!"

Download Remote Configuration

#!/bin/bash
# /docker-entrypoint-init.d/01-fetch-config.sh

set -e

CONFIG_URL="${CONFIG_URL:-}"

if [ -n "$CONFIG_URL" ]; then
    echo "Fetching configuration from $CONFIG_URL..."
    curl -sSL "$CONFIG_URL" -o /var/www/html/.env.remote

    # Merge with existing
    if [ -f /var/www/html/.env ]; then
        cat /var/www/html/.env.remote >> /var/www/html/.env
    fi
fi

Docker Compose Examples

Complete Laravel Setup

services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    volumes:
      - .:/var/www/html
      - ./docker/init:/docker-entrypoint-init.d:ro
    environment:
      DB_HOST: db
      REDIS_HOST: redis
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

Init Scripts Directory

docker/
  init/
    01-wait-for-services.sh
    02-run-migrations.sh
    03-seed-database.sh
    04-cache-warmup.sh

Kubernetes Patterns

Init Containers

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      initContainers:
        # Wait for database
        - name: wait-for-db
          image: busybox:1.36
          command: ['sh', '-c', 'until nc -z db 3306; do sleep 2; done']

        # Run migrations
        - name: migrations
          image: ghcr.io/cboxdk/php-baseimages/php-cli:8.5-bookworm-v1
          command: ['php', 'artisan', 'migrate', '--force']
          volumeMounts:
            - name: app
              mountPath: /var/www/html

      containers:
        - name: app
          image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

Lifecycle Hooks

containers:
  - name: app
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "php artisan cache:clear"]
      preStop:
        exec:
          command: ["/bin/sh", "-c", "php artisan down"]

Error Handling

By default a failing init script logs a warning and the container continues booting. Set CBOX_INIT_SCRIPTS_STRICT=true to abort startup instead - the right choice when a script prepares something the app cannot run without.

Scripts are executed, not sourced: exit inside your script ends only that script, never the container boot chain - and exported variables do NOT propagate to the services. Put runtime env in the container environment, not in init scripts.

Exit on Failure

#!/bin/bash
set -e  # Exit on any error

# Commands that must succeed
php artisan migrate --force || exit 1

Continue on Failure

#!/bin/bash
# Don't use set -e

# Non-critical operation
php artisan cache:clear || echo "WARNING: Cache clear failed"

# Critical operation - explicit check
if ! php artisan migrate --force; then
    echo "ERROR: Migration failed"
    exit 1
fi

Conditional Execution

#!/bin/bash
set -e

# Only run in production
if [ "${APP_ENV}" = "production" ]; then
    php artisan config:cache
    php artisan route:cache
fi

# Only run if flag is set
if [ "${RUN_MIGRATIONS:-false}" = "true" ]; then
    php artisan migrate --force
fi

Debugging Init Scripts

Enable Verbose Output

#!/bin/bash
set -ex  # -x prints each command

echo "DEBUG: Starting initialization"
echo "DEBUG: APP_ENV=$APP_ENV"

Check Script Execution

# List init scripts
docker exec myapp ls -la /docker-entrypoint-init.d/

# Run specific script manually
docker exec myapp /docker-entrypoint-init.d/02-migrations.sh

# Check script permissions
docker exec myapp stat /docker-entrypoint-init.d/02-migrations.sh

Watch Container Logs

# Real-time logs during startup
docker logs -f myapp

# Filter for init messages
docker logs myapp 2>&1 | grep -i "init\|migration\|cache"

Best Practices

1. Use Numbered Prefixes

01-wait.sh      # Dependencies first
02-migrate.sh   # Database changes
03-seed.sh      # Data population
04-cache.sh     # Cache warming

2. Keep Scripts Idempotent

# Good: Can run multiple times safely
php artisan migrate --force

# Bad: Fails on second run
php artisan db:seed  # Use --force or check first

3. Fail Fast

#!/bin/bash
set -e  # Stop on first error

# Critical operations at the top
php artisan migrate --force

# Non-critical at the bottom
php artisan cache:clear || true

4. Log Everything

#!/bin/bash
echo "$(date): Starting initialization..."
echo "$(date): Environment: ${APP_ENV}"
echo "$(date): Database: ${DB_HOST}"

5. Use Health Checks

services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      start_period: 60s  # Give init scripts time to complete

Next Steps