Getting Laravel 12 Logs into Coolify's Log Viewer
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 intercepterror_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:
- Laravel calls
Log::error('Something broke'). - The
errorlogchannel calls PHP'serror_log()with the formatted message. - PHP-FPM's worker process emits the error via its internal error channel.
catch_workers_output = yestells the FPM master to grab this output.- FPM master writes it to its stderr.
- Supervisord captures FPM's stderr and forwards it to
/dev/stderr. - Docker captures the container's stderr.
- 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
| Variable | Value | Purpose |
|---|---|---|
LOG_CHANNEL | stack | Use the stack driver (default) |
LOG_STACK | daily,errorlog | Write to daily files AND error_log() |
LOG_LEVEL | debug | Or error in production if you want less noise |
Files Changed
| File | Purpose |
|---|---|
docker/php-fpm.conf | New — enables FPM worker output capture |
Dockerfile | COPY the FPM config to the right path |
.env / Coolify env vars | LOG_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.