The workspace editor hung for 60s and then 504'd. Three independent faults were
stacked behind that one symptom.
Reachability: editor_target delegated to proxy_target, which returns
CONTAINER_PROXY_HOST plus the published host port and never falls back to the
container. From inside the app container that address crosses docker0 into the
host INPUT chain, whose policy is DROP with an allow-list that does not include
the published port range, so the packet was dropped and the request hung rather
than being refused. Measured from the app container: container_ip:8443 answers
302, gateway:20006 is dropped. One shared reachable_target now prefers the
direct container leg and falls back to the published port, and editor_target
uses tunnel_target as services/containers/CLAUDE.md already required. The same
defect affected /p/{slug} ingress and every tunnel, since all three resolved
through proxy_target.
The recorded measurement that motivated the old order (container_ip times out,
gateway connects) no longer holds: make docker-attach puts the app on the
instances' bridge network, which is what makes the direct leg work.
Duplicate response headers: the forwarding core relayed the upstream Date and
Server alongside the ones the serving layer generates, so every proxied
response carried two of each. Both are singleton headers and duplicating them
is malformed HTTP.
Serialization: WorkspaceViewOut declared flag_reason and three sibling strings
as str, so a NULL column made the workspace page 500 for JSON clients.
Documents the two public hostnames and the devplace.net SSH tunnel, so a future
session does not conclude the site is down after pointing curl --resolve at an
address the hostname does not resolve to, and adds the layered procedure for
diagnosing a production failure.
Verified on production with Playwright over both hostnames: the code-server
login renders and the workbench loads. Suite: 3345 passed.
236 lines
8.7 KiB
Plaintext
236 lines
8.7 KiB
Plaintext
upstream app {
|
|
server app:10500;
|
|
}
|
|
|
|
# A non-upgrade request maps to an EMPTY Connection header, not "close", so the
|
|
# catch-all can use this map and still keep the upstream connection alive.
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' '';
|
|
}
|
|
|
|
map $uri $upload_disposition {
|
|
default "attachment";
|
|
"~*\.(jpe?g|png|gif|webp|bmp|tiff|mp4|webm|ogv|mov|m4v|mp3)$" "inline";
|
|
}
|
|
|
|
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=${NGINX_CACHE_MAX_SIZE} inactive=60m use_temp_path=off;
|
|
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name _;
|
|
|
|
client_max_body_size ${NGINX_MAX_BODY_SIZE};
|
|
|
|
add_header X-Content-Type-Options nosniff;
|
|
add_header X-XSS-Protection "1; mode=block";
|
|
add_header Referrer-Policy strict-origin-when-cross-origin;
|
|
|
|
gzip on;
|
|
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
|
|
gzip_min_length 1000;
|
|
gzip_vary on;
|
|
gzip_proxied any;
|
|
|
|
location /static/uploads/ {
|
|
alias /data/uploads/;
|
|
add_header X-Content-Type-Options nosniff;
|
|
add_header Content-Disposition $upload_disposition;
|
|
add_header Cache-Control "public, max-age=604800";
|
|
access_log off;
|
|
log_not_found off;
|
|
}
|
|
|
|
location ~ ^/static/v\d+/(?<asset>.+)$ {
|
|
alias /app/static/$asset;
|
|
add_header X-Content-Type-Options nosniff;
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable, max-age=31536000";
|
|
access_log off;
|
|
log_not_found off;
|
|
}
|
|
|
|
location /static/ {
|
|
alias /app/static/;
|
|
expires 1h;
|
|
add_header Cache-Control "public, max-age=3600";
|
|
access_log off;
|
|
log_not_found off;
|
|
}
|
|
|
|
location /devii/ws {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# SEO Diagnostics live progress websocket (/tools/seo/<uid>/ws).
|
|
location ~ ^/tools/seo/[^/]+/ws$ {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# Database API async query progress websocket (/dbapi/query/<uid>/ws).
|
|
location ~ ^/dbapi/query/[^/]+/ws$ {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# Pub/Sub bus websocket (/pubsub/ws).
|
|
location = /pubsub/ws {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# Container interactive shell websocket (/projects/<slug>/containers/instances/<uid>/exec/ws).
|
|
location ~ ^/projects/[^/]+/containers/instances/[^/]+/exec/ws$ {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# Workspace editor: code-server HTTP assets AND its websocket share one prefix
|
|
# (/projects/<slug>/containers/instances/<uid>/code/...), so this location must
|
|
# carry both. The catch-all sets Connection "" and would break the editor.
|
|
location ~ ^/projects/[^/]+/containers/instances/[^/]+/code(/.*)?$ {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_buffering off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# Real-time direct messages websocket (/messages/ws).
|
|
location = /messages/ws {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# DeepSearch live progress and grounded chat websockets (/tools/deepsearch/<uid>/ws|chat).
|
|
location ~ ^/tools/deepsearch/[^/]+/(ws|chat)$ {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
location /avatar/ {
|
|
proxy_pass http://app;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Container ingress (/p/<slug>): proxies HTTP and WebSocket to a published
|
|
# container service. Handles upgrade for WS and uses long timeouts.
|
|
location /p/ {
|
|
proxy_pass http://app;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_buffering off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
}
|
|
|
|
# XML-RPC bridge (/xmlrpc): forwards XML-RPC method calls to the app, which
|
|
# reverse-proxies them to the forking XML-RPC server. Long timeouts because a
|
|
# single multicall can fan out into many internal REST calls.
|
|
location /xmlrpc {
|
|
proxy_pass http://app;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
|
|
location / {
|
|
proxy_pass http://app;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_http_version 1.1;
|
|
# Tunnel hosts (*.tunnel.<domain>) are dispatched by the app and carry
|
|
# arbitrary user apps, which routinely use websockets. They arrive here,
|
|
# so the catch-all must forward the upgrade instead of dropping it -
|
|
# dropping it is what closes a tunnelled websocket with status 1006.
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
|
|
${NGINX_CACHE_CONFIG}
|
|
}
|
|
}
|