I’ve been experimenting recently with Dockerized PHP applications, trying to move away from file based error logs and instead send everything to stdout. That way, all logs appear neatly when I run docker logs
. It worked but then I noticed something odd: the same PHP error was showing up twice. Once in the PHP-FPM container logs, and once in the Nginx container logs. At first I thought I’d misconfigured something. But after digging, I realized this is actually expected behavior. Let me walk you through the setup, trigger an error on purpose, explain why it appears in both places, and finally show you how I built a practice project to play with different error levels and fixes.
The Baseline Dockerized PHP-FPM + Nginx
Here’s my starting point: a very standard Docker Compose setup with Nginx and PHP-FPM.
docker-compose.yml
version: "3.9"
services:
web:
image: nginx:latest
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
- ./default.conf:/etc/nginx/conf.d/default.conf
links:
- php-fpm
php-fpm:
image: php:8-fpm
volumes:
- ./src:/var/www/html
default.conf
server {
index index.php index.html;
server_name phpfpm.local;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
src/index.php
<?php
ini_set('display_errors', 'off');
ini_set('error_log', '/dev/stdout');
error_log('test');
echo phpinfo();
With this setup, PHP logs go to stdout (so docker logs php-fpm
shows them). But at the same time, Nginx also writes out a message like:
FastCGI sent in stderr: "PHP message: test"
Triggering Error on Purpose
To really understand what’s going on, I updated my index.php
to deliberately trigger a few different errors:
src/index.php
<?php
/**
* This script intentionally triggers errors so I can watch how
* PHP-FPM and Nginx each log them.
*/
ini_set('display_errors', 'off');
ini_set('log_errors', '1');
ini_set('error_log', '/dev/stdout');
error_log('test'); // my control log
function make_notice() {
return $undefinedVar + 1; // undefined variable
}
function make_warning() {
return 10 / 0; // division by zero
}
function make_error() {
trigger_error('This is a user-level error', E_USER_ERROR);
}
function make_exception() {
throw new RuntimeException('Uncaught exception for demo');
}
$action = $_GET['action'] ?? 'none';
switch ($action) {
case 'notice': make_notice(); break;
case 'warning': make_warning(); break;
case 'error': make_error(); break;
case 'exception':make_exception();break;
default: /* none */ break;
}
echo "<h1>PHP-FPM/Nginx Logging Demo</h1>";
echo "<p>Try: <code>?action=notice</code>, <code>?action=warning</code>, <code>?action=error</code>, <code>?action=exception</code></p>";
What I Observed
- PHP-FPM container logs → They show
NOTICE
,WARNING
, orERROR
directly, because PHP writes to stdout. - Nginx container logs → They show lines like
FastCGI sent in stderr: "PHP message: ..."
because PHP-FPM also sends error messages over stderr through FastCGI, and Nginx dutifully logs them.
So the duplication is expected. One is PHP itself logging, the other is Nginx reporting what it received over FastCGI.
Adding Practice Functionality
Once I understood the duplication, I decided to make this into a practice project where I could try different log setups.
Bootstrap with Environment Variables
I created a small bootstrap.php
file so I could toggle logging destinations and error levels without editing code:
src/bootstrap.php
<?php
$LOG_DEST = getenv('LOG_DEST') ?: 'stdout';
$LOG_FILE = getenv('LOG_FILE') ?: '/var/log/php-error.log';
$LOG_LEVEL = getenv('LOG_LEVEL') ?: 'E_ALL';
switch ($LOG_DEST) {
case 'stderr':
ini_set('error_log', '/dev/stderr');
break;
case 'file':
ini_set('error_log', $LOG_FILE);
break;
default:
ini_set('error_log', '/dev/stdout');
break;
}
ini_set('display_errors', 'off');
ini_set('log_errors', '1');
$levels = [
'E_ALL' => E_ALL,
'E_ERROR' => E_ERROR,
'E_WARNING' => E_WARNING,
'E_NOTICE' => E_NOTICE,
'E_USER_ERROR' => E_USER_ERROR,
'E_USER_WARNING' => E_USER_WARNING,
'E_USER_NOTICE' => E_USER_NOTICE,
];
$error_reporting = $levels[$LOG_LEVEL] ?? E_ALL;
error_reporting($error_reporting);
Then I required it at the top of index.php
:
<?php
require __DIR__ . '/bootstrap.php';
// rest of the teaching code…
Now I could change logging behavior in my docker-compose.yml
:
php-fpm:
image: php:8-fpm
volumes:
- ./src:/var/www/html
environment:
LOG_DEST: "stdout" # stdout|stderr|file
LOG_FILE: "/var/log/php-error.log"
LOG_LEVEL: "E_WARNING"
This gave me quick practice toggling how and what PHP logs.
Adding Utilities: /log
and /health
I extended index.php
with a couple of handy endpoints:
if (($action ?? '') === 'log') {
error_log(json_encode([
'ts' => date('c'),
'level' => 'info',
'event' => 'practice_log',
'detail' => 'manual log via ?action=log'
]));
echo "<p>Wrote a JSON log line to error_log.</p>";
}
if (($action ?? '') === 'health') {
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'ts' => time()]);
exit;
}
Now I could run:
http://localhost:8080/?action=log
→ logs a JSON linehttp://localhost:8080/?action=health
→ returns a JSON health check
Reducing Duplication in Nginx
Finally, I tested a tweak to reduce duplicate logs. In PHP-FPM’s pool config:
php-fpm-pool.conf
[www]
catch_workers_output = yes
With this mounted into the container, PHP-FPM captures worker stdout/stderr itself instead of sending everything to Nginx. That means fewer FastCGI sent in stderr
lines in the Nginx logs.
Final Thought
What I originally thought was a misconfiguration turned out to be normal behavior: PHP-FPM logs to stdout, and Nginx logs the same errors when they arrive over FastCGI stderr. By intentionally triggering errors, I could see how the logs flowed. Then, by adding environment-controlled logging and small practice utilities, I built myself a playground for experimenting with PHP logging in Docker. If you’re working with PHP-FPM and Nginx in Docker, don’t be surprised when you see errors twice. Instead, use it as an opportunity to understand how each layer logs and configure PHP-FPM (catch_workers_output
) or your logging destinations depending on what makes sense for your workflow.