fix(logging): keep laravel.log writable across container UIDs (#451)

Fixes the largest production noise cluster — **8 duplicate issues**, all
`UnexpectedValueException: The stream or file
"/app/storage/logs/laravel.log" could not be opened in append mode:
Failed to open stream: Permission denied`:

`PHP-LARAVEL-G`, `PHP-LARAVEL-Z`, `PHP-LARAVEL-12`, `PHP-LARAVEL-2P`,
`PHP-LARAVEL-2Q`, `PHP-LARAVEL-2R`, `PHP-LARAVEL-2S`, `PHP-LARAVEL-2T`.

## Root cause

`docker/entrypoint.sh` runs `migrate` / `config:cache` / etc. **as
root** before supervisor starts. With the default `umask 022`, the first
log line written during boot creates `laravel.log` as `root:root 0644`.
The persisted `whisper-storage` named volume
(`docker-compose.production.yml`) keeps that stale, non-group-writable
file across deploys — so the `www-data` php-fpm and queue workers can't
append to it and every code path that logs throws. 8 code paths → 8
Sentry issues.

## Fix

- **entrypoint**: `umask 0002`, pre-create `laravel.log` group-writable
before any artisan command, and normalize it to `0664` in the
post-artisan re-apply step.
- **config/logging.php**: `permission => 0664` on the `single` and
`daily` channels, so files Laravel creates itself (including daily
rotation) stay group-writable.

Net effect: whoever writes first (root at boot or www-data at runtime),
the file is owned `www-data` and group-writable `0664` — no more
`EACCES`.

## Tests

- `single` and `daily` channels expose `permission => 0664`.
- `bash -n docker/entrypoint.sh` passes.

## Follow-up

Once deployed and confirmed quiet, the 8 issues should be merged into
one canonical group in Sentry.

Fixes PHP-LARAVEL-G, PHP-LARAVEL-Z, PHP-LARAVEL-12, PHP-LARAVEL-2P,
PHP-LARAVEL-2Q, PHP-LARAVEL-2R, PHP-LARAVEL-2S, PHP-LARAVEL-2T.
This commit is contained in:
Víctor Falcón 2026-05-29 15:10:50 +02:00 committed by GitHub
parent 64b78e3680
commit 741dc49d53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 17 additions and 0 deletions

View File

@ -62,6 +62,7 @@ return [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'permission' => 0664,
'replace_placeholders' => true,
],
@ -70,6 +71,7 @@ return [
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'permission' => 0664,
'replace_placeholders' => true,
],

View File

@ -1,6 +1,11 @@
#!/bin/bash
set -e
# Boot-time artisan commands run as root; this keeps any file they create
# (including laravel.log) group-writable so the www-data fpm/queue workers
# can append to it afterwards.
umask 0002
echo "=== Whisper Money Container Startup ==="
# Ensure storage directories exist (volume may be empty on first deploy)
@ -10,8 +15,12 @@ mkdir -p /app/storage/framework/cache/data
mkdir -p /app/storage/framework/sessions
mkdir -p /app/storage/framework/views
mkdir -p /app/storage/logs
# Pre-create the log file group-writable so a stale root-owned file from a
# persisted volume cannot block writes from the www-data workers.
touch /app/storage/logs/laravel.log
chown -R www-data:www-data /app/storage
chmod -R 775 /app/storage
chmod 664 /app/storage/logs/laravel.log
# Auto-generate APP_KEY if not set or invalid (must start with "base64:")
if [ -z "$APP_KEY" ] || [[ ! "$APP_KEY" =~ ^base64: ]]; then
@ -88,6 +97,7 @@ php artisan storage:link 2>/dev/null || true
echo "Re-applying storage ownership..."
chown -R www-data:www-data /app/storage /app/bootstrap/cache
chmod -R 775 /app/storage /app/bootstrap/cache
chmod 664 /app/storage/logs/laravel.log
echo "=== Startup complete, launching services ==="

View File

@ -0,0 +1,5 @@
<?php
test('file log channels are created group-writable', function (string $channel) {
expect(config("logging.channels.{$channel}.permission"))->toBe(0664);
})->with(['single', 'daily']);