Laravel#
This guide walks through running every major Laravel long-running service on OORT — from scratch setup to production deployment across docker run, Docker Compose, Docker Swarm, and Kubernetes.
Throughout this page, my-laravel-app refers to the image you built with the Dockerfile on the Usage page.
Prerequisites#
Before deploying, ensure your Laravel project has:
PHP 8.5 compatibility (or your chosen OORT tag)
Redis for queues, Horizon, Reverb, Pulse, and caching
A database (PostgreSQL or MySQL) for application data
Environment variables configured (
APP_KEY,DB_*,REDIS_*, etc.)
Install Redis and database as separate containers or managed services — OORT runs only your PHP process.
Laravel Octane#
What it does
Laravel Octane boots your application once and keeps it in memory, using Swoole to handle concurrent HTTP requests with dramatically lower latency than traditional PHP-FPM.
When to use it
Use Octane as your primary HTTP entry point for web traffic, APIs, and the Pulse dashboard.
Installation#
composer require laravel/octane
php artisan octane:install --server=swoole
This publishes the Octane configuration and adds Swoole server settings.
Configuration#
The deployment examples below use curl against the /up endpoint. You must register this route in your application before health checks can succeed.
Laravel 11+ — enable the health route in bootstrap/app.php:
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
// ...
Laravel 10 and earlier — add a route that returns HTTP 200:
use Illuminate\Support\Facades\Route;
Route::get('/up', fn () => response()->noContent());
Without an active /up endpoint, probes will fail and orchestrators may restart the container.
Key environment variables:
OCTANE_SERVER=swoole
OCTANE_HTTPS=false
In config/octane.php, review max_execution_time, workers, and task_workers for your workload.
Deployment#
docker run -d \
--name laravel-octane \
-p 80:80 \
--env-file .env \
--health-cmd "curl -f http://localhost/up || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
my-laravel-app \
php artisan octane:start --server=swoole --host=0.0.0.0 --port=80
services:
octane:
image: my-laravel-app
ports:
- "80:80"
env_file: .env
command: php artisan octane:start --server=swoole --host=0.0.0.0 --port=80
depends_on:
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/up"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
services:
octane:
image: my-laravel-app
deploy:
replicas: 2
restart_policy:
condition: on-failure
update_config:
parallelism: 1
delay: 10s
ports:
- target: 80
published: 80
protocol: tcp
mode: host
env_file: .env
command: php artisan octane:start --server=swoole --host=0.0.0.0 --port=80
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/up"]
interval: 30s
timeout: 10s
retries: 3
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-octane
spec:
replicas: 2
selector:
matchLabels:
app: laravel-octane
template:
metadata:
labels:
app: laravel-octane
spec:
containers:
- name: octane
image: my-laravel-app
ports:
- containerPort: 80
command: ["php", "artisan", "octane:start", "--server=swoole", "--host=0.0.0.0", "--port=80"]
envFrom:
- secretRef:
name: laravel-env
livenessProbe:
httpGet:
path: /up
port: 80
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /up
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: laravel-octane
spec:
selector:
app: laravel-octane
ports:
- port: 80
targetPort: 80
Laravel Horizon#
What it does
Horizon provides a Redis-backed queue dashboard and a robust supervisor for Laravel queue workers. It manages worker processes, balancing, metrics, and failed-job handling.
When to use it
Use Horizon when you need managed queue workers with monitoring instead of a single queue:work process.
Installation#
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Publish and customize config/horizon.php for your queue names, worker counts, and memory limits.
Configuration#
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_PORT=6379
Run migrations so Horizon’s metadata tables exist. Protect the /horizon dashboard in production via Horizon::auth() in AppServiceProvider.
Deployment#
docker run -d \
--name laravel-horizon \
--env-file .env \
--health-cmd "php artisan horizon:status || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
my-laravel-app \
php artisan horizon
services:
horizon:
image: my-laravel-app
env_file: .env
command: php artisan horizon
depends_on:
redis:
condition: service_started
healthcheck:
test: ["CMD", "php", "artisan", "horizon:status"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
services:
horizon:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
env_file: .env
command: php artisan horizon
healthcheck:
test: ["CMD", "php", "artisan", "horizon:status"]
interval: 30s
timeout: 10s
retries: 3
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-horizon
spec:
replicas: 1
selector:
matchLabels:
app: laravel-horizon
template:
metadata:
labels:
app: laravel-horizon
spec:
containers:
- name: horizon
image: my-laravel-app
command: ["php", "artisan", "horizon"]
envFrom:
- secretRef:
name: laravel-env
livenessProbe:
exec:
command: ["php", "artisan", "horizon:status"]
initialDelaySeconds: 30
periodSeconds: 30
Laravel Reverb#
What it does
Reverb is Laravel’s first-party WebSocket server for real-time broadcasting. It integrates with Laravel Echo and supports horizontal scaling via Redis.
When to use it
Use Reverb when your application needs WebSocket connections for live updates, notifications, or chat.
Installation#
composer require laravel/reverb
php artisan reverb:install
This publishes config/reverb.php and adds the required environment variables.
Configuration#
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST=reverb
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
The deployment examples below probe http://localhost:8080/up on the Reverb server process. Recent laravel/reverb releases expose this endpoint automatically when reverb:start is running — it responds with {"health":"OK"} and requires no extra route in routes/web.php.
If probes fail after deploy, confirm your Reverb version includes the /up endpoint and that the health check port matches --port in the reverb:start command.
Deployment#
docker run -d \
--name laravel-reverb \
-p 8080:8080 \
--env-file .env \
--health-cmd "curl -f http://localhost:8080/up || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
my-laravel-app \
php artisan reverb:start --host=0.0.0.0 --port=8080
services:
reverb:
image: my-laravel-app
ports:
- "8080:8080"
env_file: .env
command: php artisan reverb:start --host=0.0.0.0 --port=8080
depends_on:
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/up"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
services:
reverb:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
ports:
- target: 8080
published: 8080
protocol: tcp
mode: host
env_file: .env
command: php artisan reverb:start --host=0.0.0.0 --port=8080
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/up"]
interval: 30s
timeout: 10s
retries: 3
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-reverb
spec:
replicas: 1
selector:
matchLabels:
app: laravel-reverb
template:
metadata:
labels:
app: laravel-reverb
spec:
containers:
- name: reverb
image: my-laravel-app
ports:
- containerPort: 8080
command: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
envFrom:
- secretRef:
name: laravel-env
livenessProbe:
httpGet:
path: /up
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /up
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: laravel-reverb
spec:
selector:
app: laravel-reverb
ports:
- port: 8080
targetPort: 8080
Laravel Queue#
What it does
queue:work runs a single Laravel queue worker process that listens on a queue connection and processes jobs one at a time (or in parallel with --workers).
When to use it
Use a standalone queue:work container when you need a simple worker without Horizon’s dashboard and process management — for example, a dedicated queue name or a lightweight setup.
Installation#
Queue support is built into Laravel. Configure your connection in config/queue.php:
QUEUE_CONNECTION=redis
Configuration#
Tune worker options for production:
--sleep=3— seconds to wait when no jobs are available--tries=3— max attempts per job--max-time=3600— restart worker after one hour (prevents memory leaks)--queue=high,default— process specific queues in priority order
Deployment#
docker run -d \
--name laravel-queue \
--env-file .env \
my-laravel-app \
php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
services:
queue:
image: my-laravel-app
env_file: .env
command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
depends_on:
redis:
condition: service_started
restart: unless-stopped
services:
queue:
image: my-laravel-app
deploy:
replicas: 2
restart_policy:
condition: on-failure
env_file: .env
command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue
spec:
replicas: 2
selector:
matchLabels:
app: laravel-queue
template:
metadata:
labels:
app: laravel-queue
spec:
containers:
- name: queue
image: my-laravel-app
command:
- php
- artisan
- queue:work
- redis
- --sleep=3
- --tries=3
- --max-time=3600
envFrom:
- secretRef:
name: laravel-env
Laravel Scheduler#
What it does
The scheduler runs recurring Artisan commands — reports, cleanups, cache refreshes, and any Schedule:: definitions in routes/console.php or app/Console/Kernel.php.
When to use it
Run schedule:work as a long-lived process (recommended in containers) instead of relying on host-level cron.
Installation#
Scheduling is built into Laravel. Define tasks in routes/console.php (Laravel 11+) or app/Console/Kernel.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')->dailyAt('01:00');
Schedule::command('cache:prune-stale-tags')->hourly();
Configuration#
schedule:work polls every minute and is ideal for container environments — no cron daemon required.
Deployment#
docker run -d \
--name laravel-scheduler \
--env-file .env \
my-laravel-app \
php artisan schedule:work
services:
scheduler:
image: my-laravel-app
env_file: .env
command: php artisan schedule:work
restart: unless-stopped
services:
scheduler:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
env_file: .env
command: php artisan schedule:work
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-scheduler
spec:
replicas: 1
selector:
matchLabels:
app: laravel-scheduler
template:
metadata:
labels:
app: laravel-scheduler
spec:
containers:
- name: scheduler
image: my-laravel-app
command: ["php", "artisan", "schedule:work"]
envFrom:
- secretRef:
name: laravel-env
Laravel Pulse#
What it does
Pulse is Laravel’s real-time application performance monitor. It collects server, queue, slow query, and usage metrics and displays them in a web dashboard (typically served through Octane).
When to use it
Use Pulse for production observability. Run pulse:work as a separate ingest worker alongside your web container.
Installation#
composer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrate
Access the dashboard at /pulse (protect it in production via Gate::define in AppServiceProvider).
Configuration#
PULSE_ENABLED=true
PULSE_STORAGE_DRIVER=redis
The pulse:work command processes incoming metric events. The dashboard itself is served by Octane (or your web server).
Deployment#
docker run -d \
--name laravel-pulse \
--env-file .env \
my-laravel-app \
php artisan pulse:work
services:
pulse:
image: my-laravel-app
env_file: .env
command: php artisan pulse:work
depends_on:
redis:
condition: service_started
restart: unless-stopped
services:
pulse:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
env_file: .env
command: php artisan pulse:work
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-pulse
spec:
replicas: 1
selector:
matchLabels:
app: laravel-pulse
template:
metadata:
labels:
app: laravel-pulse
spec:
containers:
- name: pulse
image: my-laravel-app
command: ["php", "artisan", "pulse:work"]
envFrom:
- secretRef:
name: laravel-env
Complete Laravel Stack#
This example runs every Laravel service on OORT in a single stack: Octane (HTTP), Horizon (queues), Reverb (WebSockets), Scheduler, Pulse, plus Redis and PostgreSQL.
Stack Overview#
Service |
Role |
Port |
|---|---|---|
|
HTTP application server (Swoole) |
80 |
|
Queue supervisor |
|
|
WebSocket server |
8080 |
|
Scheduled tasks |
|
|
Metrics ingest worker |
|
|
Queues, cache, broadcasting |
6379 |
|
Application database |
5432 |
Start dependencies first, then each Laravel service in its own container:
docker network create laravel-net
docker run -d --name redis --network laravel-net redis:alpine
docker run -d --name postgres --network laravel-net \
-e POSTGRES_DB=laravel -e POSTGRES_PASSWORD=secret postgres:16-alpine
docker run -d --name octane --network laravel-net -p 80:80 --env-file .env \
my-laravel-app php artisan octane:start --server=swoole --host=0.0.0.0 --port=80
docker run -d --name horizon --network laravel-net --env-file .env \
my-laravel-app php artisan horizon
docker run -d --name reverb --network laravel-net -p 8080:8080 --env-file .env \
--health-cmd "curl -f http://localhost:8080/up || exit 1" \
--health-interval=30s --health-timeout=10s --health-retries=3 \
my-laravel-app php artisan reverb:start --host=0.0.0.0 --port=8080
docker run -d --name scheduler --network laravel-net --env-file .env \
my-laravel-app php artisan schedule:work
docker run -d --name pulse --network laravel-net --env-file .env \
my-laravel-app php artisan pulse:work
services:
octane:
image: my-laravel-app
ports:
- "80:80"
env_file: .env
command: php artisan octane:start --server=swoole --host=0.0.0.0 --port=80
depends_on:
redis:
condition: service_started
postgres:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/up"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
horizon:
image: my-laravel-app
env_file: .env
command: php artisan horizon
depends_on:
redis:
condition: service_started
healthcheck:
test: ["CMD", "php", "artisan", "horizon:status"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
reverb:
image: my-laravel-app
ports:
- "8080:8080"
env_file: .env
command: php artisan reverb:start --host=0.0.0.0 --port=8080
depends_on:
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/up"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
scheduler:
image: my-laravel-app
env_file: .env
command: php artisan schedule:work
restart: unless-stopped
pulse:
image: my-laravel-app
env_file: .env
command: php artisan pulse:work
depends_on:
redis:
condition: service_started
restart: unless-stopped
redis:
image: redis:alpine
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: laravel
POSTGRES_USER: laravel
POSTGRES_PASSWORD: secret
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
services:
octane:
image: my-laravel-app
deploy:
replicas: 2
restart_policy:
condition: on-failure
ports:
- target: 80
published: 80
protocol: tcp
mode: host
env_file: .env
command: php artisan octane:start --server=swoole --host=0.0.0.0 --port=80
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/up"]
interval: 30s
timeout: 10s
retries: 3
horizon:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
env_file: .env
command: php artisan horizon
reverb:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
ports:
- target: 8080
published: 8080
protocol: tcp
mode: host
env_file: .env
command: php artisan reverb:start --host=0.0.0.0 --port=8080
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/up"]
interval: 30s
timeout: 10s
retries: 3
scheduler:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
env_file: .env
command: php artisan schedule:work
pulse:
image: my-laravel-app
deploy:
replicas: 1
restart_policy:
condition: on-failure
env_file: .env
command: php artisan pulse:work
redis:
image: redis:alpine
deploy:
replicas: 1
restart_policy:
condition: on-failure
postgres:
image: postgres:16-alpine
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
POSTGRES_DB: laravel
POSTGRES_USER: laravel
POSTGRES_PASSWORD: secret
apiVersion: v1
kind: Secret
metadata:
name: laravel-env
type: Opaque
stringData:
APP_KEY: "base64:your-app-key"
APP_ENV: production
DB_CONNECTION: pgsql
DB_HOST: postgres
DB_PORT: "5432"
DB_DATABASE: laravel
DB_USERNAME: laravel
DB_PASSWORD: secret
REDIS_HOST: redis
REDIS_PORT: "6379"
QUEUE_CONNECTION: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-octane
spec:
replicas: 2
selector:
matchLabels:
app: laravel-octane
template:
metadata:
labels:
app: laravel-octane
spec:
containers:
- name: octane
image: my-laravel-app
ports:
- containerPort: 80
command: ["php", "artisan", "octane:start", "--server=swoole", "--host=0.0.0.0", "--port=80"]
envFrom:
- secretRef:
name: laravel-env
livenessProbe:
httpGet:
path: /up
port: 80
initialDelaySeconds: 15
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: laravel-octane
spec:
selector:
app: laravel-octane
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-horizon
spec:
replicas: 1
selector:
matchLabels:
app: laravel-horizon
template:
metadata:
labels:
app: laravel-horizon
spec:
containers:
- name: horizon
image: my-laravel-app
command: ["php", "artisan", "horizon"]
envFrom:
- secretRef:
name: laravel-env
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-reverb
spec:
replicas: 1
selector:
matchLabels:
app: laravel-reverb
template:
metadata:
labels:
app: laravel-reverb
spec:
containers:
- name: reverb
image: my-laravel-app
ports:
- containerPort: 8080
command: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
envFrom:
- secretRef:
name: laravel-env
livenessProbe:
httpGet:
path: /up
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /up
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: laravel-reverb
spec:
selector:
app: laravel-reverb
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-scheduler
spec:
replicas: 1
selector:
matchLabels:
app: laravel-scheduler
template:
metadata:
labels:
app: laravel-scheduler
spec:
containers:
- name: scheduler
image: my-laravel-app
command: ["php", "artisan", "schedule:work"]
envFrom:
- secretRef:
name: laravel-env
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-pulse
spec:
replicas: 1
selector:
matchLabels:
app: laravel-pulse
template:
metadata:
labels:
app: laravel-pulse
spec:
containers:
- name: pulse
image: my-laravel-app
command: ["php", "artisan", "pulse:work"]
envFrom:
- secretRef:
name: laravel-env
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: laravel
- name: POSTGRES_USER
value: laravel
- name: POSTGRES_PASSWORD
value: secret
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432