Files
projectsend/docker/production/nginx.conf
T
ignacionelson f9e08412f2 Still log a failing health check in the production image
#1804 dropped every /up request from the nginx access log, so the
container's health checks stopped flooding `docker logs`. That also hid
the failing ones: when the container goes unhealthy, the 5xx from /up is
the line someone looks for, and Docker's health status alone does not say
why.

Key the map on the status as well as the path, so only a 2xx /up is
dropped. Verified against the 2.6.0 image: a 503 /up logs, a 200 /up does
not, and a 200 /upload still logs.
2026-10-01 15:24:08 -03:00

96 lines
4.0 KiB
Nginx Configuration File

# Production server block for the official image.
#
# Kept deliberately close to docker/web/nginx.conf — the dev stack's config
# — because the security properties below were reviewed as a set. Two
# differences, both forced by this
# being one container instead of two:
#
# 1. fastcgi_pass targets 127.0.0.1, not the `app` compose service.
# 2. There is no user/uid coordination to do here, because the Dockerfile
# has already done it: it points the `user` directive in the package's
# own nginx.conf at www-data, so nginx and php-fpm are the same user
# and storage/ needs no group juggling. Without that step nginx runs
# as the alpine package's `nginx` (uid 100) and cannot read what
# php-fpm just wrote — see the comment on that line.
# The image's HEALTHCHECK hits /up every 30 seconds, which buried
# `docker logs` under two lines per check. Drop a passing check from the
# access log; a failing one (anything but a 2xx) still logs, because that
# is the line someone reads when the container goes unhealthy.
map "$request_uri:$status" $loggable {
~^/up:2 0;
default 1;
}
server {
listen 80 default_server;
server_name _;
root /var/www/html/public;
index index.php;
# Overrides the http-level access_log only to apply $loggable above.
access_log /dev/stdout main if=$loggable;
# Uploads arrive in chunks (Uppy resumable), so this caps a single
# chunk, not a file. Raising it does not raise the maximum file size.
client_max_body_size 100m;
# The nginx version number is nobody's business but ours — matches
# docker/web/nginx.conf.
server_tokens off;
# Baseline hardening for every response. `always` so they survive error
# responses too. NOTE: nginx does not merge add_header across levels —
# a location that declares any add_header of its own inherits none of
# these, so /protected-files/ below repeats them deliberately.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Protected file serving: PHP authorizes, nginx streams.
# PHP responds with X-Accel-Redirect: /protected-files/<path>.
#
# This is the only location that returns bytes someone else uploaded,
# so it gets the strictest headers in the file. `sandbox` puts anything
# rendered as a document into an opaque origin with scripts disabled —
# if a payload ever does reach here with a renderable content type
# (FileThumbnailController's allowlist is the primary defence), it
# cannot touch this app's origin or the viewer's session. Images loaded
# as subresources are unaffected: a CSP on a subresource response never
# creates a browsing context, so thumbnails and previews still render.
# `^~` so this prefix beats the `\.php$` regex below: without it a
# protected path ending in .php would be handed to the PHP handler
# instead of streaming under the sandbox headers this block sets.
location ^~ /protected-files/ {
internal;
alias /var/www/html/storage/app/files/;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "sandbox; default-src 'none'" always;
}
location ~ \.php$ {
# Never hand a path to PHP-FPM that isn't a real script on disk:
# without this, any URI ending in .php reaches the interpreter and
# PATH_INFO resolution decides what actually runs.
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
}
location ~ /\.(?!well-known) {
deny all;
}
}