Playbook: Pinbox24 nginx-proxy S3 Round-Robin 404 Bug
Created: 2026-07-07
Incident: api.w4.pinbox24.com alternating 200/404/CORS — every other request fails
Root cause: s3-v42-prod had VIRTUAL_HOST=api.w4.pinbox24.com identical to v42-prod, causing nginx-proxy round-robin to both containers
Symptom pattern
- Browser network panel shows alternating 200/404 for the same endpoints (
/api/i18n/langs,/api/profile/workspaces, etc.) - CORS errors on ~50% of requests (S3 service lacks CORS headers for w4.pinbox24.com origin)
- Specific pattern: every 2nd request fails — classic round-robin signature
- PM2 shows stable (no restarts, single process online) — NOT a crash loop
Root cause
jwilder/nginx-proxy groups all containers with the same VIRTUAL_HOST into one upstream and round-robins ALL requests between them. When s3-v42-prod (the OLD S3 microservice, v4-s3 image) shares VIRTUAL_HOST=api.w4.pinbox24.com with the Node.js backend (v42-prod):
upstream api.w4.pinbox24.com {
# v42-prod — handles /api/* correctly → 200
server 172.18.0.6:3000;
# s3-v42-prod — handles /upload/* only, returns 404 for all other /api/*
server 172.18.0.4:3000;
}
nginx round-robins ALL requests → 50% hit the S3 service → 404 or CORS error.
Confirm diagnosis
ssh root@94.23.26.113 'docker exec nginx-proxy grep -A20 "^upstream api.w4.pinbox24.com {" /etc/nginx/conf.d/default.conf | head -22'
# BUG: shows both v42-prod AND s3-v42-prod
# FIXED: shows only v42-prodAlso confirm the S3 container’s VIRTUAL_HOST:
ssh root@94.23.26.113 'docker exec s3-v42-prod printenv VIRTUAL_HOST 2>/dev/null'
# BUG: returns "api.w4.pinbox24.com"
# FIXED: no output (VIRTUAL_HOST not set)Fix
Remove VIRTUAL_HOST and LETSENCRYPT_HOST from s3-environment.env, then recreate the container.
Step 1 — Remove VIRTUAL_HOST from s3-environment.env
# Use Python — sed has quoting issues when called from PowerShell here-strings
$script = @"
import re
path = '/root/builds/7N4sbbrB/0/pinbox24/p24-back-ts/s3-environment.env'
with open(path) as f:
lines = f.readlines()
filtered = [l for l in lines if not re.match(r'^(VIRTUAL_HOST|LETSENCRYPT_HOST)=', l)]
with open(path, 'w') as f:
f.writelines(filtered)
print('removed', len(lines) - len(filtered), 'lines')
"@
$script | ssh root@94.23.26.113 'python3 /dev/stdin'
# Expected: "removed 2 lines"Step 2 — Recreate s3-v42-prod
ssh root@94.23.26.113 'cd /root/builds/7N4sbbrB/0/pinbox24/p24-back-ts && CONTAINER_NAME=v42-prod docker-compose up -d --force-recreate --no-build s3 2>&1'
# Expected: "Recreating s3-v42-prod ... done"Step 3 — Restart nginx-proxy
ssh root@94.23.26.113 'docker restart nginx-proxy 2>&1'
# Expected: "nginx-proxy"Step 4 — Verify upstream
ssh root@94.23.26.113 'sleep 3 && docker exec nginx-proxy grep -A12 "upstream api.w4.pinbox24.com {" /etc/nginx/conf.d/default.conf | head -14'
# Expected: ONLY "# v42-prod" in upstream, no "# s3-v42-prod"Step 5 — Test API
1..5 | ForEach-Object {
$r = Invoke-WebRequest "https://api.w4.pinbox24.com/api/i18n/langs" -TimeoutSec 10 -UseBasicParsing -ErrorAction SilentlyContinue
Write-Host "Request $_ : $($r.StatusCode)"
}
# Expected: 200 200 200 200 200What does s3-v42-prod do after this fix?
s3-v42-prod (OLD v4-s3 image) remains running and accessible within prod-v-4-net. It is still reachable from v42-prod at http://s3-v42-prod:3000 for internal Docker network calls. Only the external nginx routing is removed. This preserves:
- Internal S3 proxy calls from v42-prod (if any)
- File upload handling for paths the backend delegates internally
- The
bms1-post-restart-recovery.mdstep 3 still starts this container — that’s fine
After bms-1 restart
The bms1-post-restart-recovery.md playbook starts s3-v42-prod via docker start s3-v42-prod. Since the container now has no VIRTUAL_HOST, nginx-proxy will NOT add it to any upstream on startup. This is the correct behavior.
Prevention
- The
infra-src/pinbox24/w4/docker-compose.ymlin p24-infra should document thats3-environment.envmust NOT setVIRTUAL_HOST=api.w4.pinbox24.com. This env var was set when the OLD S3 service was intended to receive file upload requests directly from nginx. With theuploadAwsS3.helper.jspersistent patch in v42-prod handling uploads directly, the S3 service no longer needs to be reachable externally. - TODO (issue #3161 follow-up): migrate W4 to use
s3-v2-api.w4.pinbox24.comfor all S3 operations and removes3-v42-prodentirely (tracked in CI/CD redesign).
References
- Issue #3161 — v42-prod crash + CORS root cause investigation
- PR #3163 — PM2 fork mode fix (partial fix for the same incident)
pinbox24-w3-w4-outage-diagnosis.md— general W4 outage diagnosisbms1-post-restart-recovery.md— container start sequence after bms-1 restart
Audit Log
$env:SUPABASE_URL = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
python -c "
import os, sys
sys.path.insert(0, 'C:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('claude', 'config_change', 's3-v42-prod', 'success', 'Removed VIRTUAL_HOST=api.w4.pinbox24.com from s3-environment.env — nginx round-robin 404 fix', 'bms-1', 3163)
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''