skip to content

How do you get a Laravel 13 app's logs into a container's stderr, and why can entries still vanish when the app runs under PHP-FPM?

level: seniorimportance: should knowfreq 45%

answer

  1. skeleton defines a stderr channel
  2. monolog driver on php://stderr
  3. files die with the container
  4. FPM catch_workers_output defaults to no
  5. emergency logger still writes a file

basics

~10 s

Point logging at the skeleton's stderr channel with LOG_CHANNEL=stderr or LOG_STACK=stderr; it is a Monolog StreamHandler on php://stderr. Under PHP-FPM, worker stderr is discarded unless the pool sets catch_workers_output = yes.

solid answer

~40 s

In a container, files under `storage/logs` are invisible to the platform's log collector and disappear with the container, so logs should go to stdout or stderr. The skeleton already defines a `stderr` channel: the `monolog` driver with `StreamHandler` on `php://stderr`, level from `LOG_LEVEL`, and a formatter from `LOG_STDERR_FORMATTER`. Select it with `LOG_CHANNEL=stderr`, or `LOG_STACK=stderr` to keep the default stack. Queue workers, the scheduler and other Artisan processes are CLI processes whose stderr reaches the container when they run as its main process. PHP-FPM is different: its workers' stdout and stderr go to `/dev/null` unless the pool sets `catch_workers_output = yes`, and `decorate_workers_output = no` removes FPM's prefix from each line. Also watch for silent fallbacks: a bad channel or level makes Laravel's emergency logger write to `storage/logs/laravel.log` inside the container, where nobody looks.

code

ini · 3 lines
ini
LOG_CHANNEL=stack
LOG_STACK=stderr
LOG_LEVEL=info

go deeper

for a junior

Know that containers should log to stderr and that the skeleton already has a stderr channel.

for a middle

Explain how the stderr channel is built with the monolog driver and how to select it with LOG_CHANNEL or LOG_STACK.

for a senior

Diagnose missing web-request logs under PHP-FPM, fix the pool settings, and catch emergency-logger fallbacks inside the container.

for a principal

Set a platform-wide logging contract: stderr only, one entry per line, and a deploy check that proves logs arrive.

## Why files are the wrong target in containers A container's filesystem is private and usually temporary. Log lines written to `storage/logs/laravel.log` are not collected by the orchestrator's log pipeline, are lost when the container is replaced, and can fill the writable layer. The container convention is to write logs to the process's **stdout or stderr** and let the platform collect them. ## The skeleton's stderr channel Laravel 13's `config/logging.php` already contains the channel you need: ```php 'stderr' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, 'handler_with' => ['stream' => 'php://stderr'], 'formatter' => env('LOG_STDERR_FORMATTER'), 'processors' => [PsrLogMessageProcessor::class], ], ``` - It uses the **`monolog` driver**: Laravel builds any Monolog handler class, here `StreamHandler`, with the constructor arguments in `handler_with`. - `LOG_STDERR_FORMATTER` lets you choose a formatter class, typically one that emits one JSON object per line, without editing the file. - It reads `LOG_LEVEL` like the file channels. Two ways to select it: 1. `LOG_CHANNEL=stderr` makes it the default channel directly. 2. `LOG_STACK=stderr` keeps `stack` as the default and swaps its member, which leaves room to add others later, such as `LOG_STACK=stderr,slack`. Avoid `LOG_STACK=single,stderr` in containers unless you truly want a second copy in the container's filesystem. ## Which process is writing? A Laravel container often runs several kinds of PHP process, and they reach stderr differently: | Process | Where `php://stderr` goes | |---|---| | `php artisan queue:work`, `schedule:work`, other Artisan commands | The process's stderr, which is the container's when it is the main process | | PHP-FPM workers serving HTTP | `/dev/null` unless the pool enables `catch_workers_output` | | Long-running application servers run as CLI processes | Their own stderr, as for Artisan | ## The PHP-FPM gap PHP-FPM runs a master process and a pool of worker processes. By default a worker's stdout and stderr are **redirected to `/dev/null`**, following the FastCGI convention. So an app that logs perfectly to stderr from `queue:work` can produce nothing for web requests. The fix is in the FPM pool configuration, not in Laravel: - `catch_workers_output = yes` redirects worker stdout and stderr into FPM's main error log; - `decorate_workers_output = no` stops FPM from wrapping each line with a worker prefix, level and time, which otherwise breaks one-JSON-object-per-line parsing; - FPM's own error log must itself point at the container's stderr for the lines to leave the container. An alternative is the `errorlog` driver, which calls PHP's `error_log()`; where that ends up also depends on the PHP and FPM configuration, so it needs the same checking. ## Silent fallbacks to a file Even with stderr configured, Laravel can still write to a file. If a channel cannot be built, for example because `LOG_LEVEL` holds an invalid name or `LOG_STACK` names a channel that does not exist, the log manager falls back to an **emergency logger** that writes to `storage/logs/laravel.log` (or the `emergency` channel's `path`). In a container that means logs quietly stop appearing in the platform. After deploying logging changes, check both the platform's output and the container's `storage/logs` directory. ## Volume, levels and alerts Moving to stderr changes the economics of logging. Files on a disk cost little until they fill it; a log pipeline often charges by volume and indexes every line. So: - run production at `LOG_LEVEL=info` or `warning`, and reserve `debug` for short investigations; - remember that `LOG_LEVEL` also feeds any other skeleton channel you stack beside stderr, including the `slack` channel's level; - keep alerting as a separate stack member, such as `LOG_STACK=stderr,slack` with Slack's level hard-coded to `critical`, rather than building alerts on top of a flood of info lines; - avoid writing the same entry twice through overlapping channels, which doubles cost and confuses counts. ## Checklist - `LOG_CHANNEL=stderr` or `LOG_STACK=stderr` in the container's environment. - FPM pool with `catch_workers_output = yes` and `decorate_workers_output = no`. - A line-per-entry formatter chosen through `LOG_STDERR_FORMATTER`. - A smoke test that writes one entry from a web request and one from a queue worker and confirms both arrive.

  • Why do queue worker logs appear in the container output while web request logs do not?
    `queue:work` is a CLI process, so `php://stderr` is its own stderr and reaches the container. PHP-FPM workers send stdout and stderr to `/dev/null` by default; the pool must set `catch_workers_output = yes`, and FPM's error log must point at the container's stderr.
  • How can a Laravel app configured for stderr still end up writing to a file?
    When a channel cannot be built, for example because of an invalid `LOG_LEVEL` or an undefined channel in `LOG_STACK`, the log manager falls back to an emergency logger that writes to `storage/logs/laravel.log`. In a container that file is easy to miss.

saying these in an interview costs you the question

  • PHP-FPM forwards worker stderr to the container by default
  • Laravel has a dedicated stderr driver separate from monolog
  • Writing to storage/logs is fine because containers keep their files
  • Once LOG_CHANNEL=stderr is set, Laravel never writes a log file
  • decorate_workers_output has no effect on JSON log parsing