Switch Fly proxy to upstream keepalive pools #337

Merged
eblume merged 6 commits from fly-proxy-keepalive into main 2026-04-17 16:39:52 -07:00
2 changed files with 57 additions and 21 deletions
Showing only changes of commit 6a1d9cc0bf - Show all commits

Switch Fly proxy to upstream keepalive pools

Replace per-request DNS resolution (variable-based proxy_pass) with
static upstream blocks and keepalive connection pools. This reuses
TLS connections through the Tailscale tunnel instead of handshaking
per request, which should significantly reduce latency at >1 req/s.

Trade-off: DNS is resolved at config load, not per-request. If
Tailscale Ingress pods get new IPs, run `mise run fly-reload` to
re-resolve.

Also adds mise-tasks/fly-reload for nginx config reload without
full redeploy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Erich Blume 2026-04-17 15:42:57 -07:00

View file

@ -46,18 +46,32 @@ http {
proxy_cache_path /tmp/cache levels=1:2 keys_zone=services:10m
max_size=200m inactive=24h;
# MagicDNS resolver using a variable in proxy_pass defers upstream DNS
# resolution to request time (not config time). Results are cached for
# 30s per worker to avoid per-request DNS lookups.
# WebSocket-aware Connection header. Only send "upgrade" when the client
# actually requests a protocol switch; otherwise empty string to preserve
# upstream keepalive connections.
map $http_upgrade $connection_upgrade {
default "";
websocket upgrade;
}
# --- Upstream pools with keepalive ---
# DNS is resolved once at config load via MagicDNS. If Tailscale Ingress
# pods get new IPs (restart, reschedule), run `mise run fly-reload` to
# re-resolve. A Grafana alert fires when upstreams are unreachable.
resolver 100.100.100.100 valid=30s;
resolver_timeout 5s;
# WebSocket-aware Connection header. Only send "upgrade" when the client
# actually requests a protocol switch; otherwise "close" (the HTTP/1.1
# default when keepalive pooling is not available).
map $http_upgrade $connection_upgrade {
default close;
websocket upgrade;
upstream forge_backend {
server forge.tail8d86e.ts.net:443;
keepalive 8;
}
upstream docs_backend {
server docs.tail8d86e.ts.net:443;
keepalive 4;
}
upstream cv_backend {
server cv.tail8d86e.ts.net:443;
keepalive 4;
}
# --- docs.eblu.me (static site) ---
@ -76,12 +90,14 @@ http {
internal;
}
location / {
set $upstream_docs https://docs.tail8d86e.ts.net;
proxy_pass $upstream_docs$request_uri;
proxy_pass https://docs_backend$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_intercept_errors on;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
# Cache aggressively static site only.
# Do NOT use these settings for dynamic services.
proxy_cache services;
@ -116,12 +132,14 @@ http {
}
location / {
set $upstream_cv https://cv.tail8d86e.ts.net;
proxy_pass $upstream_cv$request_uri;
proxy_pass https://cv_backend$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_intercept_errors on;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_cache services;
proxy_cache_valid 200 1d;
proxy_cache_valid 404 1m;
@ -187,8 +205,7 @@ http {
location ~ ^/user/(login|sign_up|forgot_password) {
limit_req zone=forge_auth burst=5 nodelay;
set $upstream_forge https://forge.tail8d86e.ts.net;
proxy_pass $upstream_forge$request_uri;
proxy_pass https://forge_backend$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_intercept_errors on;
@ -206,11 +223,13 @@ http {
# Cache release artifact downloads immutable files keyed by tag+filename.
# Avoids hammering Forgejo when crawlers or users re-download the same asset.
location ~ ^/[^/]+/[^/]+/releases/download/ {
set $upstream_forge_releases https://forge.tail8d86e.ts.net;
proxy_pass $upstream_forge_releases$request_uri;
proxy_pass https://forge_backend$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_cache services;
proxy_cache_valid 200 7d;
proxy_cache_key $host$uri;
@ -226,11 +245,13 @@ http {
# Selectively cache static assets only
location ~* \.(css|js|png|jpg|svg|woff2?)$ {
set $upstream_forge_static https://forge.tail8d86e.ts.net;
proxy_pass $upstream_forge_static$request_uri;
proxy_pass https://forge_backend$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_cache services;
proxy_cache_valid 200 7d;
proxy_cache_key $host$uri;
@ -240,8 +261,7 @@ http {
}
location / {
set $upstream_forge https://forge.tail8d86e.ts.net;
proxy_pass $upstream_forge$request_uri;
proxy_pass https://forge_backend$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_intercept_errors on;

16
mise-tasks/fly-reload Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
#MISE description="Reload Fly.io proxy nginx config (re-resolves upstream DNS)"
set -euo pipefail
export FLY_API_TOKEN
FLY_API_TOKEN="$(op read 'op://blumeops/fly.io admin/add more/deploy-token')"
# SSH into the Fly machine and send nginx a reload signal.
# This re-resolves upstream DNS without a full redeploy.
APP="blumeops-proxy"
MACHINE_ID=$(fly machines list -a "$APP" --json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
echo "Reloading nginx on machine $MACHINE_ID..."
fly ssh console -a "$APP" -C "nginx -s reload"
echo "Done. Upstream DNS re-resolved."