Stu Mason
Stu Mason

Getting Laravel 12 Logs into Coolify's Log Viewer

Stu Mason5 min read

Coolify shows container logs — anything written to stdout/stderr by the container process. By default, a Laravel app running behind PHP-FPM writes logs to , which is a file inside the container. Coolify never sees it. This guide explains how to get Laravel errors, warnings, and application logs...

Coolify shows container logs — anything written to stdout/stderr by the container process. By default, a Laravel app running behind PHP-FPM writes logs to storage/logs/laravel.log, which is a file inside the container. Coolify never sees it.

This guide explains how to get Laravel errors, warnings, and application logs appearing in Coolify's log viewer alongside your nginx access logs and supervisor output.

Why LOG_CHANNEL=stderr Doesn't Work

The obvious answer is to set LOG_CHANNEL=stderr or add stderr to your LOG_STACK. Laravel's stderr channel writes to php://stderr. Simple.

Except it doesn't work under PHP-FPM.

PHP-FPM worker processes have their file descriptors redirected. When a worker writes to php://stderr, it doesn't go to the container's stderr — it goes to /dev/null (or FPM's internal error handling, depending on configuration). This is a fundamental aspect of how FPM manages worker processes, not a Laravel bug.

You can verify this yourself: set LOG_STACK=stderr, trigger an error, and watch Coolify's logs. Nothing appears. Check storage/logs/ — nothing there either. Your logs are vanishing.

Writing directly to /dev/stderr (the device file, not the PHP stream) also doesn't work. Same redirection.

The Fix: errorlog Channel + FPM Worker Output Capture

The solution has two parts that work together.

Part 1: Use the errorlog Channel

Laravel's errorlog channel uses PHP's native error_log() function. Unlike direct stream writes, error_log() goes through PHP-FPM's error handling pipeline, which FPM can be configured to forward.

Set your environment variable:

LOG_STACK=daily,errorlog

This gives you both: file-based daily logs inside the container (for tail -f debugging via terminal) and error_log() output that FPM can capture.

Part 2: Tell PHP-FPM to Capture Worker Output

By default, PHP-FPM discards output from error_log() in workers. You need to explicitly tell it to capture and forward this output.

Create docker/php-fpm.conf:

[www]
catch_workers_output = yes
decorate_workers_output = no

Two settings, that's it:

  • catch_workers_output = yes — tells FPM to intercept error_log() output from worker processes and forward it to the FPM master process's stderr.
  • decorate_workers_output = no — prevents FPM from wrapping each log line with its own timestamp and metadata prefix. Without this, every Laravel log line gets an ugly [pool www] child 42 said into stderr: wrapper that breaks JSON parsing and makes the output unreadable.

Part 3: Copy the Config in Your Dockerfile

# PHP-FPM config: forward worker output to container stderr
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/zz-laravel.conf

The zz- prefix ensures this file loads last, overriding any defaults from the base image. The path /usr/local/etc/php-fpm.d/ is the standard FPM pool config directory in official PHP Docker images.

Part 4: Supervisord Wiring

Your supervisord config for php-fpm needs stderr piped to the container's stderr:

[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

This is how most Laravel Docker setups already work, but verify yours has it. The chain is:

Laravel error_log() → FPM worker → FPM master (catch_workers_output) → /dev/stderr → supervisord → container stderr → Docker → Coolify

The Full Chain

Here's what happens when Laravel logs an error:

  1. Laravel calls Log::error('Something broke').
  2. The errorlog channel calls PHP's error_log() with the formatted message.
  3. PHP-FPM's worker process emits the error via its internal error channel.
  4. catch_workers_output = yes tells the FPM master to grab this output.
  5. FPM master writes it to its stderr.
  6. Supervisord captures FPM's stderr and forwards it to /dev/stderr.
  7. Docker captures the container's stderr.
  8. Coolify reads Docker's log stream and displays it.

Remove any link in that chain and your logs vanish silently.

What About JSON Formatting?

If you want structured JSON logs in Coolify (useful for log aggregation), add:

LOG_STDERR_FORMATTER=Monolog\Formatter\JsonFormatter

Wait — this configures the stderr channel's formatter, not the errorlog channel. If you're using errorlog (which you should be), this env var does nothing.

To get JSON output from the errorlog channel, override it in config/logging.php:

'errorlog' => [
    'driver' => 'errorlog',
    'level' => env('LOG_LEVEL', 'debug'),
    'replace_placeholders' => true,
    'formatter' => \Monolog\Formatter\JsonFormatter::class,
],

Or just leave it as plain text. Coolify's log viewer handles plain text fine, and it's more readable when you're tailing logs at 10pm trying to figure out why Passport won't authenticate.

Environment Variables Summary

VariableValuePurpose
LOG_CHANNELstackUse the stack driver (default)
LOG_STACKdaily,errorlogWrite to daily files AND error_log()
LOG_LEVELdebugOr error in production if you want less noise

Files Changed

FilePurpose
docker/php-fpm.confNew — enables FPM worker output capture
DockerfileCOPY the FPM config to the right path
.env / Coolify env varsLOG_STACK=daily,errorlog

Common Mistakes

Using stderr instead of errorlog: The stderr channel writes directly to php://stderr or /dev/stderr. Neither works under FPM because workers' file descriptors are redirected. Use errorlog.

Forgetting catch_workers_output: Even with the errorlog channel, FPM discards worker output by default. Both sides are required.

Leaving decorate_workers_output on: FPM wraps every line with [pool www] child 123 said into stderr: "...". This breaks JSON formatters and makes multi-line stack traces unreadable.

Setting env vars that get overridden by duplicates: If your deployment platform (like Coolify) has duplicate environment variables, the last one wins. Check that LOG_STACK isn't being overridden by a duplicate set to daily.

Expecting logs immediately: After deploying, the FPM config change requires a fresh container. FPM reads pool configs at startup, not at runtime. A redeploy/restart is required — you can't hot-reload this.

Get the Friday email

What I shipped this week, what I learned, one useful thing.

No spam. Unsubscribe anytime. Privacy policy.