Playbook: nginx-proxy client_max_body_size — fix 413 on Pinbox uploads (bms-1)
Status: ACTIVE
Server: bms-1 (94.23.26.113) — Pinbox24 production (P0 — handle with care)
Container: nginx-proxy (nginxproxy/nginx-proxy:latest)
First applied: 2026-07-12 (issue #3986)
Symptom
Uploads to a Pinbox vhost fronted by nginx-proxy fail with HTTP 413 Request Entity
Too Large for bodies larger than 1 MB. The 413 is emitted by nginx before the
request reaches the backend app. Typical trigger: the n8n W4 Direct API Ingestion
workflow (MUjApqruo6H88ebw) POSTing invoice PDFs (~2–3 MB) to
POST https://api.w4.pinbox24.com/api/offices/files/upload.
nginx-proxy error log shows:
client intended to send too large body: 3035792 bytes, server: api.w4.pinbox24.com
Root cause
nginx-proxy sets no client_max_body_size directive, so nginx uses its built-in
default of 1 MB (1m = 1,048,576 bytes). Any upload over 1 MB is rejected at the
proxy layer.
Confirm the default is in effect (should print nothing):
# on bms-1 as root
docker exec nginx-proxy grep -rns client_max_body_size /etc/nginx/Why a global conf.d snippet (not a per-vhost vhost.d/<host> file)
nginx-proxy’s config layout on bms-1 uses host bind mounts under
/docker/vol/etc/nginx/ (certs, conf.d, vhost.d), so anything written there
persists across container recreation. Two candidate mechanisms exist:
| Approach | Persists | Needs reload only | Companion-safe | Notes |
|---|---|---|---|---|
conf.d/*.conf global snippet (chosen) | ✅ | ✅ | ✅ | http-context; applies to all vhosts (a ceiling, harmless) |
vhost.d/<host> per-vhost file | ✅ | ❌ needs docker-gen regeneration | ❌ | see caveats below |
The per-vhost route has two traps on this host:
- Either/or include. nginx-proxy emits either
include /etc/nginx/vhost.d/<host>;orinclude /etc/nginx/vhost.d/default;— never both. The generatedconf.d/default.confcurrently referencesdefault. Creating a newvhost.d/api.w4.pinbox24.comfile only takes effect after docker-gen regeneratesdefault.conf(a Docker container event), which is more invasive on a P0 host. You would also have to copyvhost.d/default’s letsencrypt ACME block into the new file to avoid dropping it. - Companion clobber. The
jrcs/letsencrypt-nginx-proxy-companionsidecar ownsvhost.d/defaultand may rewrite it, discarding any directive appended there.
The conf.d/*.conf snippet avoids both: nginx.conf has include /etc/nginx/conf.d/*.conf;
at http context; docker-gen only rewrites default.conf (never other .conf files);
the companion never touches conf.d. A single graceful reload applies it.
A single 50m ceiling for all vhosts is acceptable — it is a maximum, not a reservation,
and it also pre-empts the same latent 413 on w3.pinbox24.com.
Fix
# 1. Write the snippet into the conf.d bind mount (persists across recreation)
CONF=/docker/vol/etc/nginx/conf.d/client_max_body_size.conf
cat > "${CONF}.tmp" <<'EOF'
# Issue #3986 — raise upload body limit (nginx default 1m rejected ~3MB W4 invoice PDFs with 413).
# Global (http context); applies to all nginx-proxy vhosts. Persisted via /docker/vol bind mount.
# Survives docker-gen regen (only rewrites default.conf) and letsencrypt-companion writes (own vhost.d/default).
client_max_body_size 50m;
EOF
mv "${CONF}.tmp" "$CONF" && chmod 600 "$CONF"
# 2. Validate BEFORE reloading (ssl_stapling OCSP warnings are pre-existing / harmless)
docker exec nginx-proxy nginx -t
# 3. Graceful reload (no restart, no downtime)
docker exec nginx-proxy nginx -s reload
# 4. Confirm the directive is live in the running config
docker exec nginx-proxy nginx -T 2>/dev/null | grep client_max_body_sizeTune 50m to the largest legitimate upload with margin.
Verify (proxy-layer, no credentials)
# on bms-1 — send bodies of known size straight at the local nginx for the vhost
UP="https://api.w4.pinbox24.com/api/offices/files/upload"
RES="--resolve api.w4.pinbox24.com:443:127.0.0.1"
head -c 2000000 /dev/zero | tr '\0' 'A' > /tmp/blob2m.txt # 2 MB
curl -sk $RES -o /dev/null -w '%{http_code}\n' -X POST -F "file=@/tmp/blob2m.txt" "$UP"
# Expect: NOT 413 (401/400/404 from backend = body accepted by nginx). Pre-fix: 413.
head -c 60000000 /dev/zero | tr '\0' 'A' > /tmp/blob60m.txt # 60 MB
curl -sk $RES -o /dev/null -w '%{http_code}\n' -X POST -F "file=@/tmp/blob60m.txt" "$UP"
# Expect: 413 (above the 50m ceiling — proves the directive governs this vhost).
rm -f /tmp/blob2m.txt /tmp/blob60m.txtThen re-run the real workflow (MUjApqruo6H88ebw) or wait for the :20 trigger and confirm
the upload_* nodes return 200 and no new too large body lines appear in
docker logs nginx-proxy.
Note — upstream app body limit
An unauthenticated proxy-layer probe returns 401 (auth middleware runs before the
body parser), so it cannot confirm whether the W4 Node app imposes its own smaller
express/multer limits.fileSize. Issue #3986’s evidence attributes the 413 to
nginx-proxy specifically (server: api.w4.pinbox24.com in the nginx error log), so the
nginx fix resolves the reported failure. If a legitimately-authenticated upload still
fails on size after this fix, inspect the W4 app’s multer/body-parser limits next.
Rollback
rm /docker/vol/etc/nginx/conf.d/client_max_body_size.conf
docker exec nginx-proxy nginx -t && docker exec nginx-proxy nginx -s reload
# nginx reverts to the built-in 1m default.Related
docs/playbooks/nginx-proxy-upgrade-jwilder-to-nginxproxy.md— nginx-proxy layout, bind mounts, image swap- Issue #3986 — original 413 report + n8n workflow context