All checks were successful
deploy / Build api + worker (static musl) (push) Successful in 6m59s
deploy / Deploy moments-worker to frootmig (push) Successful in 19s
deploy / Deploy moments-api to nikola (push) Successful in 32s
deploy / Build prerendered web (push) Successful in 4m38s
deploy / Deploy web to oolon (push) Successful in 21s
refresh / Rebuild prerendered web (push) Successful in 4m51s
refresh / Deploy refreshed web to oolon (push) Successful in 39s
Any route asked for without a trailing slash sent the visitor to a port nothing answers on from outside: $ curl -sSI https://rob.tn/activity HTTP/2 301 location: https://rob.tn:14443/activity/ `try_files $uri $uri/` 301s a slash-less directory URL to add the slash, and nginx builds that Location as an absolute URL from its own $server_port. This vhost listens on WEB_LISTEN — 127.0.0.1:14443, behind the edge's stream SNI router — so the redirect advertised 14443 instead of the 443 the client used. The browser then sat on a TCP connect that never completes and gave up only after its own timeout, 60s+, before showing an error. It reads as the site hanging. `absolute_redirect off` makes the Location relative, so the client keeps whatever scheme, host and port it actually used. `port_in_redirect off` would drop the port too, but this also stops nginx asserting a scheme and host it cannot know from behind the router. Every /activity, /blog, /cv and /project/... request without the trailing slash was affected — external links, bookmarks, typed URLs, crawlers. In-app navigation never round-trips to the server, and `/` needs no directory redirect, which is why the homepage always loaded fine and this stayed hidden. It is not a regression from any recent change; it follows from the vhost listening on a shifted port. Verified by reproducing the port leak in a container listening on 8081 published as 18081 — `Location: http://127.0.0.1:8081/activity/` before the directive, `Location: /activity/` after — then rendering the real template through script/render-site-conf.py (9 placeholders in, none surviving) and passing `nginx -t` on the result. Closes #9
80 lines
2.8 KiB
Cheetah
80 lines
2.8 KiB
Cheetah
upstream moments_api {
|
|
server {{API_UPSTREAM_ADDR}} max_fails=3 fail_timeout=30s;
|
|
keepalive 8;
|
|
}
|
|
|
|
server {
|
|
server_name {{SERVER_NAME}};
|
|
# Behind the edge's stream SNI router — see WEB_LISTEN in deploy.yml for why
|
|
# this must not bind 443 directly. real_ip recovery lives in the edge's
|
|
# conf.d/proxy-protocol.conf.
|
|
listen {{WEB_LISTEN}};
|
|
http2 on;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/{{SERVER_NAME}}/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/{{SERVER_NAME}}/privkey.pem;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
|
|
root {{WEB_ROOT}};
|
|
index index.html;
|
|
|
|
# Emit relative Location headers. `try_files $uri $uri/` 301s a slash-less
|
|
# directory URL (/activity -> /activity/), and by default nginx builds that
|
|
# redirect from its own $server_port — WEB_LISTEN's 14443, not the 443 the
|
|
# client used, because this vhost sits behind the edge's stream SNI router.
|
|
# So /activity redirected visitors to https://rob.tn:14443/activity/, where
|
|
# nothing answers from outside, and the browser hung until its connect
|
|
# timeout (60s+) before failing. Relative redirects keep whatever scheme,
|
|
# host and port the client actually used.
|
|
absolute_redirect off;
|
|
|
|
# Compress text responses on the wire. text/html is always compressed when
|
|
# gzip is on (nginx won't let it be listed in gzip_types); the prerendered
|
|
# pages are large — the dashboard bakes the full all-time activity dataset —
|
|
# so the HTML alone drops from ~900 KB to ~90 KB. gzip_proxied any also
|
|
# compresses the JSON from the /api/ upstream. woff2 is already compressed,
|
|
# so it's intentionally not listed.
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_comp_level 6;
|
|
gzip_min_length 1024;
|
|
gzip_proxied any;
|
|
gzip_types
|
|
text/css
|
|
text/javascript
|
|
text/plain
|
|
text/xml
|
|
application/javascript
|
|
application/json
|
|
application/manifest+json
|
|
application/xml
|
|
image/svg+xml
|
|
font/ttf;
|
|
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
add_header Cache-Control "no-cache" always;
|
|
}
|
|
|
|
location ~* ^(?!/api/)\S+\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp|avif)$ {
|
|
expires 30d;
|
|
add_header Cache-Control "public, max-age=2592000, immutable";
|
|
try_files $uri =404;
|
|
}
|
|
|
|
location /api/ {
|
|
rewrite ^/api/(.*)$ /$1 break;
|
|
proxy_pass {{API_UPSTREAM_SCHEME}}://moments_api;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_read_timeout 30s;
|
|
proxy_connect_timeout 5s;
|
|
}
|
|
|
|
access_log /var/log/nginx/{{SERVER_NAME}}.access.log;
|
|
error_log /var/log/nginx/{{SERVER_NAME}}.error.log;
|
|
}
|