Skip to content

Commit 3ef6912

Browse files
authored
feat: add worker mode support for horizontal scaling (#3903)
1 parent fd63749 commit 3ef6912

File tree

12 files changed

+947
-146
lines changed

12 files changed

+947
-146
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ LOG_STDOUT=false
124124
SESSION_DRIVER=file
125125
SESSION_LIFETIME=120
126126

127-
# `sync` if jobs needs to be executed live (default) or `database` if they can be defered.
127+
# `sync` if jobs need to be executed live (default) or `database` if they can be deferred.
128128
QUEUE_CONNECTION=sync
129+
# Choose this mode only if you have set up a queue worker (strongly recommended though).
130+
# QUEUE_CONNECTION=database
129131

130132
SECURITY_HEADER_HSTS_ENABLE=false
131133
SECURITY_HEADER_CSP_CONNECT_SRC=

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,8 @@ EXPOSE 8000
140140
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
141141

142142
# Default command: run Octane with FrankenPHP
143+
# Container mode is controlled by LYCHEE_MODE environment variable:
144+
# - "web" (default): Runs FrankenPHP/Octane web server (this CMD)
145+
# - "worker": Runs Laravel queue worker (entrypoint.sh overrides CMD)
146+
# See docs/specs/2-how-to/deploy-worker-mode.md for deployment guide
143147
CMD ["php", "artisan", "octane:start", "--server=frankenphp", "--host=0.0.0.0", "--port=8000"]

docker-compose.yaml

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ services:
121121

122122
# Cache
123123
CACHE_STORE: "${CACHE_STORE:-file}"
124-
CACHE_PREFIX: "${CACHE_PREFIX:-lychee_keygen_cache}"
124+
CACHE_PREFIX: "${CACHE_PREFIX:-lychee_cache}"
125125
REDIS_HOST: "lychee_cache"
126126
REDIS_PASSWORD: "null"
127127
REDIS_PORT: "6379"
@@ -142,8 +142,6 @@ services:
142142
LOG_LEVEL: "${LOG_LEVEL:-debug}"
143143
LOG_STDOUT: "${LOG_STDOUT:-true}"
144144

145-
# QUEUE_CONNECTION:sync
146-
147145
# SECURITY_HEADER_HSTS_ENABLE:false
148146
# SECURITY_HEADER_CSP_CONNECT_SRC:
149147
# SECURITY_HEADER_SCRIPT_SRC_ALLOW:
@@ -288,6 +286,131 @@ services:
288286
networks:
289287
- lychee
290288

289+
# Queue Worker Service (disabled by default)
290+
# Uncomment to enable horizontal scaling of background job processing
291+
lychee_worker:
292+
image: lychee-frankenphp:latest
293+
# build:
294+
# context: ./app
295+
# dockerfile: Dockerfile
296+
# args:
297+
# NODE_ENV: "${NODE_ENV:-production}"
298+
container_name: lychee-worker
299+
restart: unless-stopped # Auto-restart at container level (outer layer)
300+
301+
# Security hardening (match lychee_api)
302+
security_opt:
303+
- no-new-privileges:true
304+
- seccomp:unconfined
305+
cap_drop:
306+
- ALL
307+
cap_add:
308+
- CHOWN
309+
- SETGID
310+
- SETUID
311+
- DAC_OVERRIDE
312+
read_only: false # Laravel needs write access to storage/cache
313+
tmpfs:
314+
- /tmp:noexec,nosuid,nodev,size=100m
315+
316+
# Resource limits (adjust based on workload)
317+
# Workers typically need less CPU but more memory for image processing
318+
deploy:
319+
resources:
320+
limits:
321+
cpus: '2'
322+
memory: 2G
323+
reservations:
324+
cpus: '0.5'
325+
memory: 512M
326+
327+
env_file:
328+
- path: ./.env
329+
required: true
330+
environment:
331+
PUID: "${PUID:-1000}"
332+
PGID: "${PGID:-1000}"
333+
334+
# WORKER MODE CONFIGURATION
335+
# Set LYCHEE_MODE=worker to enable queue worker mode
336+
LYCHEE_MODE: worker
337+
338+
# Queue configuration (CRITICAL for worker mode)
339+
# Recommended: redis for production (faster, better concurrency)
340+
# Alternative: database (no Redis dependency)
341+
QUEUE_CONNECTION: "${QUEUE_CONNECTION:-database}"
342+
343+
# Queue priority (optional)
344+
# Process high-priority jobs first, then default, then low
345+
# Example: QUEUE_NAMES=high,default,low
346+
# QUEUE_NAMES: "${QUEUE_NAMES:-default}"
347+
348+
# Worker restart interval (optional)
349+
# Restart worker after N seconds to mitigate memory leaks
350+
# Default: 3600 seconds (1 hour)
351+
# WORKER_MAX_TIME: "${WORKER_MAX_TIME:-3600}"
352+
353+
# Application (inherit from lychee_api)
354+
APP_NAME: "${APP_NAME:-Lychee}"
355+
APP_ENV: "${APP_ENV:-production}"
356+
APP_DEBUG: "${APP_DEBUG:-false}"
357+
APP_TIMEZONE: "${TIMEZONE:-UTC}"
358+
APP_URL: "${APP_URL:-http://localhost:8000}"
359+
360+
# Database (same as lychee_api - shared database required)
361+
DB_CONNECTION: "${DB_CONNECTION:-mysql}"
362+
DB_HOST: "${DB_HOST:-lychee_db}"
363+
DB_PORT: "${DB_PORT:-3306}"
364+
DB_DATABASE: "${DB_DATABASE:-lychee}"
365+
DB_USERNAME: "${DB_USERNAME:-lychee}"
366+
# DB_PASSWORD comes from .env file
367+
368+
# Redis (if using redis queue driver)
369+
# Must point to same Redis instance as lychee_api
370+
REDIS_HOST: "lychee_cache"
371+
REDIS_PASSWORD: "null"
372+
REDIS_PORT: "6379"
373+
374+
# Cache (same as lychee_api)
375+
CACHE_STORE: "${CACHE_STORE:-file}"
376+
CACHE_PREFIX: "${CACHE_PREFIX:-lychee_cache}"
377+
378+
# Logging (output to stdout/stderr for container log aggregation)
379+
LOG_CHANNEL: "${LOG_CHANNEL:-stack}"
380+
LOG_STACK: "${LOG_STACK:-single}"
381+
LOG_LEVEL: "${LOG_LEVEL:-debug}"
382+
LOG_STDOUT: "${LOG_STDOUT:-true}"
383+
384+
# Shared volumes with lychee_api (CRITICAL for photo processing)
385+
# Workers need access to the same uploads and storage as web service
386+
volumes:
387+
- ./lychee/uploads:/app/public/uploads
388+
- ./lychee/storage/app:/app/storage/app
389+
- ./lychee/logs:/app/storage/logs
390+
- ./lychee/tmp:/app/storage/tmp
391+
- .env:/app/.env:ro
392+
393+
depends_on:
394+
lychee_db:
395+
condition: service_healthy
396+
397+
# Worker health check
398+
# Verifies queue:work process is running
399+
healthcheck:
400+
test: ["CMD-SHELL", "pgrep -f 'queue:work' || exit 1"]
401+
interval: 30s
402+
timeout: 10s
403+
retries: 3
404+
start_period: 60s # Give worker time to start up
405+
406+
networks:
407+
- lychee
408+
#
409+
# SCALING WORKERS:
410+
# To run multiple worker containers (horizontal scaling):
411+
# docker compose up -d --scale lychee_worker=3
412+
# This will create 3 worker containers processing jobs in parallel.
413+
291414
lychee_cache:
292415
image: redis:alpine
293416
hostname: lychee_cache

docker/scripts/entrypoint.sh

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,78 @@ php artisan view:cache
7777

7878
echo "✅ Application ready!"
7979

80-
# Execute the main command
81-
exec "$@"
80+
# Detect LYCHEE_MODE and execute appropriate command
81+
LYCHEE_MODE=${LYCHEE_MODE:-web}
82+
83+
case "$LYCHEE_MODE" in
84+
web)
85+
echo "🌐 Starting Lychee in web mode..."
86+
# Execute the main command (from Dockerfile CMD: octane:start)
87+
exec "$@"
88+
;;
89+
worker)
90+
echo "⚙️ Starting Lychee in worker mode..."
91+
echo "🔄 Auto-restart enabled: worker will restart if it exits"
92+
93+
# Get queue configuration from environment
94+
QUEUE_NAMES=${QUEUE_NAMES:-default}
95+
WORKER_MAX_TIME=${WORKER_MAX_TIME:-3600}
96+
QUEUE_CONNECTION=${QUEUE_CONNECTION:-sync}
97+
98+
echo "📋 Queue names: $QUEUE_NAMES"
99+
echo "⏱️ Max time: ${WORKER_MAX_TIME}s"
100+
echo "📡 Queue connection: $QUEUE_CONNECTION"
101+
102+
# Warn if using sync driver (not recommended for worker mode)
103+
if [ "$QUEUE_CONNECTION" = "sync" ]; then
104+
echo "⚠️ WARNING: QUEUE_CONNECTION=sync is not recommended for worker mode."
105+
echo " Jobs will run synchronously, defeating the purpose of a queue worker."
106+
echo " Consider using 'redis' or 'database' for persistent asynchronous queues."
107+
fi
108+
109+
# Track if we should keep running
110+
KEEP_RUNNING=true
111+
112+
# Handle graceful shutdown
113+
trap 'echo "🛑 Received shutdown signal, stopping..."; KEEP_RUNNING=false' TERM INT # Auto-restart loop: if queue:work exits, restart it
114+
115+
# This handles memory leak mitigation (max-time) and crash recovery
116+
while $KEEP_RUNNING; do
117+
echo "🚀 Starting queue worker ($(date '+%Y-%m-%d %H:%M:%S'))"
118+
119+
# Default exit code to 0
120+
EXIT_CODE=0
121+
122+
# Run queue worker with standard options
123+
# --tries=3: retry failed jobs up to 3 times
124+
# --timeout=3600: kill job if it runs longer than 1 hour
125+
# --sleep=3: sleep 3 seconds when queue is empty
126+
# --max-time=$WORKER_MAX_TIME: restart worker after N seconds (memory leak mitigation)
127+
php artisan queue:work \
128+
--queue="$QUEUE_NAMES" \
129+
--tries=3 \
130+
--timeout=3600 \
131+
--sleep=3 \
132+
--max-time="$WORKER_MAX_TIME" || EXIT_CODE=$?
133+
134+
if [ $EXIT_CODE -eq 0 ]; then
135+
echo "✅ Queue worker exited cleanly (exit code 0)"
136+
else
137+
echo "⚠️ Queue worker exited with code $EXIT_CODE"
138+
fi
139+
140+
# Exit if we received shutdown signal
141+
if ! $KEEP_RUNNING; then
142+
echo "👋 Shutting down worker..."
143+
exit $EXIT_CODE
144+
fi
145+
146+
echo "⏳ Waiting 5 seconds before restart..."
147+
sleep 5
148+
done
149+
;;
150+
*)
151+
echo "❌ ERROR: Invalid LYCHEE_MODE: $LYCHEE_MODE. Must be 'web' or 'worker'."
152+
exit 1
153+
;;
154+
esac

0 commit comments

Comments
 (0)