Configuration

Logging

Every line tagged with its service, JSON output for log shippers, or one file per service.

Everything logs to the container's stdout and stderr by default, and every line says which service wrote it, so a single docker logs stream stays readable and filterable.

[entrypoint] INFO: Running as non-root (UID 1000). Starting nginx + php-fpm directly.
[php-fpm] [21-Oct-2026 17:49:03] NOTICE: ready to handle connections
[nginx] 172.17.0.1 - - [21/Oct/2026:17:49:07 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.7.1"

Streams and tags

TagStreamContent
[nginx]stdoutaccess log (combined format)
[nginx]stderrNginx errors and warnings
[php-fpm]stderrFPM master and pool messages, PHP error_log() output, slow-request traces
[cron]stderrcrond output (root mode only)
[entrypoint]stdoutstartup messages from the entrypoint scripts
(untagged)stderrSupervisor's own messages in root mode, such as INFO spawned: 'nginx'
docker logs -f app 2>&1                        # everything
docker logs app 2>&1 | grep '^\[php-fpm\]'     # one service
docker logs app 2>&1 | grep -v '^\[nginx\]'    # everything except the access log

Three switches

VariableDefaultOptions
LOG_MODEstdoutstdout streams everything to the container. file writes one file per service under /var/log.
LOG_FORMATtexttext for tagged lines, json for one JSON object per line.
LOG_TAGtruefalse removes the [service] prefixes for anyone with an existing parser.

JSON mode

LOG_FORMAT=json is for Loki, Datadog, CloudWatch and similar. Service messages become objects with time, service, stream and message, and the Nginx access log switches to a structured json_combined format:

{"time":"2026-10-21T17:48:05+00:00","service":"nginx","stream":"access","remote_addr":"172.17.0.1","x_forwarded_for":"","host":"localhost","method":"GET","uri":"/err.php","protocol":"HTTP/1.1","status":200,"bytes_sent":12,"request_time":0.002,"upstream_time":"0.002","referer":"","user_agent":"curl/8.7.1"}
{"time":"2026-10-21T17:48:00+0000","service":"php-fpm","stream":"stderr","message":"[21-Oct-2026 17:48:00] NOTICE: fpm is running, pid 42"}
docker logs app 2>&1 | jq -c 'select(.service == "nginx" and .status >= 500)'

Every emitted line is valid JSON, including entrypoint messages.

File mode

LOG_MODE=file writes untagged files and is useful when a sidecar or agent tails files:

FileContent
/var/log/nginx/access.logaccess log
/var/log/nginx/error.logNginx errors
/var/log/php-fpm/error.logFPM and PHP errors
/var/log/php-fpm/slow.logslow requests
/var/log/cron.log, /var/log/supervisord.logroot mode only

Mount /var/log to keep them, and tail them in place with the bundled helper:

docker exec app container-logs -f php-fpm     # nginx | php-fpm | cron | supervisor | all

In stdout mode the same command prints the docker logs recipes above.

Individual destinations

NGINX_ACCESS_LOG, NGINX_ERROR_LOG, PHP_FPM_ERROR_LOG and PHP_FPM_SLOWLOG override single destinations whatever the mode. NGINX_ACCESS_LOG=off disables the access log entirely.

How it works

Each service is started through run-service <name> <command>, which pipes the process's stdout and stderr through a small awk filter that prefixes or JSON-wraps each line and flushes immediately. Lines that are already JSON objects pass through untouched, which is how the Nginx json_combined access log stays structured. Supervisor's own messages are left untagged because wrapping Supervisor would double-tag the output it relays from its programs.