Some checks failed
deploy / Build prerendered web (push) Successful in 7m14s
deploy / Deploy web to oolon (push) Successful in 18s
deploy / Build api + worker (static musl) (push) Successful in 5m41s
deploy / Deploy moments-worker to frootmig (push) Successful in 19s
deploy / Deploy moments-api to nikola (push) Successful in 24s
refresh / Rebuild prerendered web (push) Successful in 7m15s
refresh / Deploy refreshed web to oolon (push) Failing after 26s
oolon's TCP 443 belongs to the stream SNI router, which ssl_prereads the
handshake and forwards to the local https tier on 127.0.0.1:14443 with
PROXY protocol. site.conf.tmpl predates that and still bound 443 itself,
so every deploy and every daily refresh rsynced a vhost that collides
with the router.
Nothing in the pipeline caught it. `nginx -t` only detects duplicate
listeners within a context, not across http{} and stream{}, and
`systemctl reload` merely sends SIGHUP, so it exits 0 while nginx logs
"bind() to 0.0.0.0:443 failed (98: Address already in use) ... still
could not bind()", aborts the reconfiguration and keeps its old cycle.
The deploy went green while oolon's running config was frozen. It stayed
frozen for a day, stranding every cert the step@ timers renewed on disk
until eleven internal vhosts were serving expired certs. A cold start
would have failed outright, taking the whole public edge down.
Template the listen line from WEB_LISTEN (manifest web.config.listen for
script/deploy.sh, which renders the same template), and assert that the
reload landed by requiring a fresh worker generation, dumping the nginx
error log when it did not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182wzZE8DguMPWhxD21gfP2
70 lines
2.2 KiB
Cheetah
70 lines
2.2 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 {{DOCROOT}};
|
|
index index.html;
|
|
|
|
# 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;
|
|
}
|