Table of Contents
- The Problem
- The Root Cause
- Check Production Settings First
- Fix 1: PHP Timeout Settings
- Fix 2: nginx FastCGI Timeouts
- Why These Numbers Matter
- Default Behavior (Broken)
- Production Behavior (Works)
- Full docker-compose.yml Example
- After Making Changes
- When to Use Unlimited vs Fixed Timeouts
- Development: Unlimited (What We Just Did)
- Production: Consider Limits
- The Takeaway
The Problem
You’re running a Laravel app in Docker (nginx + PHP-FPM), and you keep hitting 504 Gateway Timeout errors on pages that work fine in production. Long-running reports, imports, and exports all fail locally but succeed on the live server.
This is a configuration mismatch: your local Docker setup has default timeouts, but production doesn’t.
The Root Cause
Two layers have timeout settings that need to align:
- PHP execution limits: How long PHP will run before killing a script
- nginx FastCGI timeouts: How long nginx will wait for PHP-FPM to respond
When either of these times out before your code finishes, you get a 504.
Check Production Settings First
SSH into your production server and check what’s actually running:
php -i | grep -E 'max_execution_time|max_input_time|memory_limit'
You’ll probably see something like:
max_execution_time => 0
max_input_time => -1
memory_limit => -1
0 and -1 mean unlimited. Production doesn’t kill long-running scripts. Your local Docker setup probably has defaults like 30s for execution time and 60s for nginx FastCGI timeout.
Fix 1: PHP Timeout Settings
Create or update your PHP overrides file:
; docker/php/php-overrides.ini
max_execution_time = 0
max_input_time = -1
memory_limit = -1
upload_max_filesize = 50M
post_max_size = 50M
Mount this file in your docker-compose.yml:
services:
php:
image: php:8.2-fpm
volumes:
- ./docker/php/php-overrides.ini:/usr/local/etc/php/conf.d/99-overrides.ini
- ./:/var/www/html
Fix 2: nginx FastCGI Timeouts
Update your nginx site config:
# docker/nginx/site.conf
server {
listen 80;
root /var/www/html/public;
index index.php;
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Add these timeout settings
fastcgi_read_timeout 300s;
fastcgi_connect_timeout 300s;
fastcgi_send_timeout 300s;
}
}
Mount this in docker-compose.yml:
services:
nginx:
image: nginx:alpine
volumes:
- ./docker/nginx/site.conf:/etc/nginx/conf.d/default.conf
- ./:/var/www/html
ports:
- "8080:80"
Why These Numbers Matter
Default Behavior (Broken)
- PHP
max_execution_time: 30s - nginx
fastcgi_read_timeout: 60s - Your report generation: 120s
Result: PHP kills the script at 30s → nginx waits until 60s → 504 error at 60s
Production Behavior (Works)
- PHP
max_execution_time: 0 (unlimited) - nginx
fastcgi_read_timeout: 300s (5 minutes) - Your report generation: 120s
Result: PHP finishes at 120s → nginx receives response → success
Full docker-compose.yml Example
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./docker/nginx/site.conf:/etc/nginx/conf.d/default.conf
- ./:/var/www/html
depends_on:
- php
php:
image: php:8.2-fpm
volumes:
- ./docker/php/php-overrides.ini:/usr/local/etc/php/conf.d/99-overrides.ini
- ./:/var/www/html
environment:
- DB_HOST=mysql
- DB_DATABASE=laravel
- DB_USERNAME=root
- DB_PASSWORD=secret
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: laravel
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
After Making Changes
Restart your containers:
docker-compose down
docker-compose up -d
Verify PHP settings took effect:
docker-compose exec php php -i | grep max_execution_time
You should see max_execution_time => 0.
When to Use Unlimited vs Fixed Timeouts
Development: Unlimited (What We Just Did)
- Mirrors production behavior
- Prevents false negatives (things that work in prod fail locally)
- Easier debugging (long operations don’t timeout mid-execution)
Production: Consider Limits
Unlimited timeouts in production can be dangerous:
- Runaway scripts can hang forever
- Resource exhaustion under load
- Harder to detect infinite loops
If your production has unlimited timeouts and you’re seeing issues, consider:
max_execution_time = 300 ; 5 minutes
memory_limit = 512M ; Generous but not unlimited
The Takeaway
When you get 504 errors in Docker that don’t happen in production, check timeout alignment between:
- PHP execution limits (
php.iniorphp-overrides.ini) - nginx FastCGI timeouts (
fastcgi_read_timeout)
Mirror your production settings locally. Don’t debug phantom timeout issues—just make the environments match.
Leave a Reply