Laravel 10 Deployment on Docker: How One Mis‑configured .ENV Caused 10+ Seconds API Latency and Crashing Queues
If you’ve ever stared at a blinking cursor while your Laravel API drags an extra 10 seconds per request, you know the panic that follows. The culprit isn’t always a missing cache key or a rogue SQL query – sometimes it’s a single line in .env that silently throttles your whole stack.
Why This Matters
In a production Docker swarm, every millisecond counts. Customers notice latency, search rankings dip, and your queue workers start dying faster than a Chrome tab after a memory leak. A bad environment variable can cascade through PHP‑FPM, Redis, and Nginx, turning a healthy Laravel 10 app into a performance nightmare.
Common Causes of Docker‑Based Laravel Slowdowns
- Incorrect
APP_DEBUGset totruein production – forces verbose error handling. - Missing
QUEUE_CONNECTIONcausing fallback to sync driver. - Improper
REDIS_HOSTpointing to127.0.0.1inside a container, breaking cache and session storage. - Using
MAIL_MAILER=login a high‑traffic API – each request writes massive logs. - Disabled
OPCACHEor mis‑setPHP_INI_SCAN_DIRleading to recompilation on every request.
REDIS_HOST=redis that forced Laravel to fallback to the default 127.0.0.1. Inside Docker that means “look inside the PHP‑FPM container”, not the Redis service. The result? Every cache read became a DB hit, adding 10+ seconds to API response time and blowing up queue workers.Step‑by‑Step Fix Tutorial
1. Re‑create a clean .env template
# .env.example
APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:YOUR_GENERATED_KEY
APP_DEBUG=false
APP_URL=https://api.example.com
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="no-reply@example.com"
MAIL_FROM_NAME="${APP_NAME}"
2. Verify Docker Compose service names
# docker-compose.yml
version: '3.8'
services:
app:
image: ghcr.io/yourorg/laravel:10-fpm
container_name: laravel_app
env_file:
- .env
volumes:
- ./:/var/www/html
depends_on:
- mysql
- redis
networks:
- backend
nginx:
image: nginx:stable-alpine
container_name: laravel_nginx
ports:
- "80:80"
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- ./:/var/www/html
depends_on:
- app
networks:
- backend
mysql:
image: mysql:8
container_name: laravel_mysql
environment:
MYSQL_DATABASE: laravel
MYSQL_USER: laravel
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root_secret
volumes:
- mysql_data:/var/lib/mysql
networks:
- backend
redis:
image: redis:7-alpine
container_name: laravel_redis
ports:
- "6379:6379"
networks:
- backend
networks:
backend:
volumes:
mysql_data:
3. Clear compiled files and warm the cache
# Inside the app container
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear
# Re‑cache for production
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize after every deploy. It forces OPCache warm‑up and guarantees that Laravel reads the correct env values.4. Adjust PHP‑FPM and OPCache settings
# /usr/local/etc/php-fpm.d/www.conf
[www]
listen = /run/php-fpm.sock
pm = dynamic
pm.max_children = 30
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 15
php_admin_value[opcache.enable]=1
php_admin_value[opcache.memory_consumption]=256
php_admin_value[opcache.interned_strings_buffer]=16
php_admin_value[opcache.max_accelerated_files]=10000
php_admin_value[opcache.validate_timestamps]=0
5. Restart services and verify latency
# From host
docker compose down
docker compose up -d
# Simple curl test
time curl -s -o /dev/null -w "%{time_total}\n" https://api.example.com/health
VPS or Shared Hosting Optimization Tips
- CPU pinning: On a VPS, bind Docker containers to specific CPU cores using
cpuset-cpusto avoid noisy neighbor throttling. - Swap management: Disable swap on production droplets (
vm.swappiness=0) to keep PHP‑FPM memory resident. - Linux kernel sysctl:
net.core.somaxconn=65535andnet.ipv4.tcp_fin_timeout=15improve Nginx throughput. - Shared hosting: If Docker isn’t an option, use
php -d opcache.enable_cli=1 artisan config:cachein your cron jobs to mimic the same warm‑up.
Real World Production Example
Acme Payments runs a Laravel‑based payment gateway on a 4‑CPU, 8 GB VPS behind Cloudflare. Their .env originally contained:
REDIS_HOST=redis
The trailing space made Laravel treat the value as "redis " (notice the space). Since no DNS entry matches, Laravel fell back to 127.0.0.1. The result was:
- Cache miss on every request → DB hit on 200+ tables.
- Queue workers timed out after 30 seconds, causing
failed_jobstable to fill. - CPU spiked to 95 % on the PHP‑FPM container, forcing the VPS to auto‑scale (extra $30/mo).
Before vs After Results
| Metric | Before Fix | After Fix |
|---|---|---|
| Average API Latency | 12.4 s | 0.38 s |
| Queue Failures (last 24h) | 324 | 0 |
| CPU Utilization (PHP‑FPM) | 92 % | 27 % |
Security Considerations
- Never commit
.envto Git. Use Docker secrets or HashiCorp Vault for production keys. - Enable
APP_KEYrotation scripts to invalidate old sessions regularly. - Set
SESSION_SECURE_COOKIE=trueandSESSION_SAME_SITE=Strictfor HTTPS‑only APIs. - Restrict Redis to the internal Docker network and require a password even in dev.
- Run
composer auditafter eachcomposer installto catch vulnerable packages.
APP_DEBUG=true in production leaks stack traces to attackers and doubles the memory footprint of every request.Bonus Performance Tips
• Use Redis as a queue backend and enable --daemon mode in Supervisor
# /etc/supervisor/conf.d/laravel-queue.conf
[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --daemon
autostart=true
autorestart=true
numprocs=4
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/laravel/queue.log
• Enable HTTP/2 on Nginx for API payloads
# nginx/conf.d/api.conf
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.crt;
ssl_certificate_key /etc/ssl/private/api.key;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php-fpm:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 60;
}
gzip on;
gzip_types text/plain application/json;
}
FAQ
Q: Doesphp artisan config:cachework with Docker secrets?
A: Yes. Docker injects secrets as temporary files; Laravel will read them on boot. Just make sureAPP_ENV=productionso the secret values are not overwritten by local .env.
Q: My queue still crashes after fixing .env. What else to check?
A: VerifyQUEUE_CONNECTION=redis, ensuresupervisormemory limits are high enough, and inspect Redismaxmemory-policy– LRU eviction can drop jobs if the cache fills.
Final Thoughts
One misplaced space in .env turned a smoothly running Laravel 10 API into a latency monster. The lesson? Treat environment configuration with the same rigor you apply to code.
Automate validation with a simple CI step:
# .github/workflows/env-lint.yml
name: Env Lint
on: [push, pull_request]
jobs:
check-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Lint .env
run: |
grep -E '^\s*$' .env && echo "Empty lines found!" && exit 1
grep -E '[[:space:]]+$' .env && echo "Trailing spaces!" && exit 1
By catching those tiny errors early, you avoid costly production incidents, keep your queue workers humming, and stay ahead of the competition.
No comments:
Post a Comment