Demo images / Build & push demo images (push) Failing after 2m13s
The repo is source code and guidelines only. Nothing about how Archipelago's own fleet is run, or how the team works, stays in it. Untracked (kept on disk, gitignored) — 250 files: - .planning/ (199) and loop/ — internal development process - fleet operations tooling that targets specific nodes: deploy-to-target, deploy-tailscale, deploy-config-defaults, setup-target-dev, setup-aiui-server, setup-https-dev, debug-frontend, node-profile, fleet-fips-pair/unpair, image-recipe/sync-from-live.sh - image-recipe/INTEGRATION-GUIDE.md and docs/multinode-testing-plan.md, both of which are live-server workflow and fleet node inventories - the Phase 10 on-node verification and evidence records, which cite .planning/ as their evidence base KEY-05-ENTROPY-ENFORCEMENT.md was initially moved out with the other Phase 10 docs and then put back: it is cited as normative rationale from ten places in the codebase, including core/clippy.toml, which bans rand::thread_rng and points at it for the reason. That makes it a guideline, not an internal record. Node names removed from source (48 occurrences across comments, manifests and test fixtures): archi-dev-box, archy-x250*, shorty-s, framework-pt, zaza-optiplex, archi-thinkpad. Comments keep the engineering context and the date, which is what carried the meaning; the machine name did not. Three of those were live test values rather than comments and were replaced with valid stand-ins, not prose: two mDNS hostnames and a mesh peer name. An earlier pass substituted "a test node" into a hostname assertion, producing an invalid hostname; caught and fixed as test-node.local. Wipe mechanism: .local-only/manifest.txt inventories every local-only path and .local-only/wipe.sh deletes them on one confirmation, refusing to touch anything git still tracks. Both are themselves untracked, so the public repo does not carry a map of internal filenames. Verified: cargo check -p archipelago --all-features clean; archipelago-container 75/75 tests pass; appOrigin vitest 7/7; audit-secrets 5/5; every relative link in tracked markdown resolves (0 broken). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1381 lines
56 KiB
Plaintext
1381 lines
56 KiB
Plaintext
# Rate limit zones
|
|
limit_req_zone $binary_remote_addr zone=rpc:10m rate=20r/s;
|
|
limit_req_zone $binary_remote_addr zone=auth:10m rate=3r/s;
|
|
limit_req_zone $binary_remote_addr zone=peer:10m rate=10r/s;
|
|
|
|
# Resolve external domains at request time (not startup) to prevent boot failures
|
|
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
|
|
resolver_timeout 5s;
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
# IPv6 listener is REQUIRED: companion phones reach this node over the
|
|
# FIPS mesh at its fips0 ULA (http://[fdxx:…]) — without [::]:80 that
|
|
# address can never connect (found live 2026-07-23, a test node).
|
|
listen [::]:80 default_server;
|
|
server_name _;
|
|
|
|
root /opt/archipelago/web-ui;
|
|
index index.html;
|
|
|
|
# This node's CA, for devices that have not trusted it yet. Deliberately
|
|
# unauthenticated and served over plain HTTP: a device fetches this BEFORE
|
|
# it can validate the node's own certificate, so requiring HTTPS or a login
|
|
# here would be a chicken-and-egg. It is a public certificate — never a key
|
|
# — and the dashboard shows its fingerprint so it can be checked on sight.
|
|
location = /ca.crt {
|
|
alias /etc/archipelago/ssl/ca-download.crt;
|
|
default_type application/x-x509-ca-cert;
|
|
add_header Content-Disposition 'attachment; filename="archipelago-node-ca.crt"';
|
|
}
|
|
|
|
# Security headers
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
add_header X-DNS-Prefetch-Control "off" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
|
|
|
# Serve Nginx Proxy Manager HTTP-01 challenge files before the SPA fallback.
|
|
location ^~ /.well-known/acme-challenge/ {
|
|
default_type text/plain;
|
|
root /var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
|
|
location /aiui/ {
|
|
try_files $uri $uri/ /aiui/index.html;
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
}
|
|
|
|
# AIUI assets fallback — AIUI may reference /assets/ without /aiui/ prefix
|
|
location /aiui-assets/ {
|
|
alias /opt/archipelago/web-ui/aiui/assets/;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
}
|
|
|
|
# AIUI Claude API proxy (API key managed by proxy, no session gate needed)
|
|
location /aiui/api/claude/ {
|
|
proxy_pass http://127.0.0.1:3142/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_connect_timeout 120s;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
|
|
# AIUI OpenRouter API proxy (API key managed by proxy, no session gate needed)
|
|
location /aiui/api/openrouter/ {
|
|
set $upstream_1 "https://openrouter.ai/api/";
|
|
|
|
proxy_pass $upstream_1;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host openrouter.ai;
|
|
proxy_ssl_server_name on;
|
|
proxy_connect_timeout 120s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
|
|
# AIUI Ollama (local AI) proxy — localhost:11434
|
|
location /aiui/api/ollama/ {
|
|
proxy_pass http://127.0.0.1:11434/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_connect_timeout 120s;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
|
|
# AIUI web search proxy — SearXNG on port 8888
|
|
location /aiui/api/web-search {
|
|
proxy_pass http://127.0.0.1:8888/search;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 30s;
|
|
error_page 502 503 =503 @searxng_unavailable;
|
|
}
|
|
location @searxng_unavailable {
|
|
default_type application/json;
|
|
return 503 '{"error":"SearXNG is not running"}';
|
|
}
|
|
|
|
# JSON error responses — prevents leaking HTML error pages to API clients
|
|
location @backend_unavailable {
|
|
default_type application/json;
|
|
return 502 '{"error":{"code":"BACKEND_UNAVAILABLE","message":"Service temporarily unavailable"}}';
|
|
}
|
|
location @backend_timeout {
|
|
default_type application/json;
|
|
return 504 '{"error":{"code":"BACKEND_TIMEOUT","message":"Service did not respond in time"}}';
|
|
}
|
|
|
|
# Icons, favicon, manifest — always revalidate (no heuristic caching)
|
|
location ~* ^/(favicon\.ico|manifest\.webmanifest|assets/icon/) {
|
|
add_header Cache-Control "no-cache, must-revalidate";
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# Versioned Vite assets must never fall through to index.html. During OTA
|
|
# a browser can keep an old HTML shell that references now-removed hashed
|
|
# chunks; returning HTML for /assets/*.js triggers strict MIME failures.
|
|
# The immutable header must NOT use `always`: with `always` a transient
|
|
# 404 (e.g. mid web-ui swap on first boot) gets cached by the browser for
|
|
# a year and the asset stays "missing" until a hard cache clear. Without
|
|
# `always` the header applies only to 2xx/3xx; 404s are routed to a
|
|
# named location that marks them no-store so the browser retries.
|
|
location /assets/ {
|
|
try_files $uri @aiui_assets;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
}
|
|
# AIUI's built index.html emits ABSOLUTE /assets/<hashed> paths, so they
|
|
# land here, in the MAIN UI's assets dir, where they do not exist — the
|
|
# real files are in aiui/assets/. The nearby /aiui-assets/ alias names this
|
|
# exact problem in its comment but only catches a path AIUI never requests,
|
|
# so it never fixed it.
|
|
#
|
|
# A fallback rather than copying the files up one level: a frontend deploy
|
|
# replaces web-ui wholesale (update.rs preserves the aiui/ directory itself,
|
|
# not copies made into assets/), so a copy is erased by the very next deploy
|
|
# while this survives. Hash collision with a real main-UI asset is not a
|
|
# concern — Vite hashes are content-derived, and a main asset that exists is
|
|
# served by try_files before this is ever consulted.
|
|
location @aiui_assets {
|
|
rewrite ^/assets/(.*)$ /aiui/assets/$1 break;
|
|
try_files $uri =404;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
error_page 404 = @asset_missing;
|
|
}
|
|
location @asset_missing {
|
|
add_header Cache-Control "no-store" always;
|
|
return 404;
|
|
}
|
|
|
|
location ~* ^/(registerSW\.js|sw\.js|workbox-[^/]+\.js)$ {
|
|
try_files $uri =404;
|
|
add_header Cache-Control "no-cache, must-revalidate" always;
|
|
}
|
|
|
|
# Serve static files (Vue.js SPA)
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
add_header Cache-Control "no-cache, must-revalidate";
|
|
}
|
|
|
|
# Peer-to-peer node messaging (receives from other nodes over Tor)
|
|
location /archipelago/ {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 256m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 30s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# JSON-RPC endpoint. Browser GETs are navigational mistakes, so send them
|
|
# back to the dashboard while keeping RPC POSTs proxied to the backend.
|
|
location = /rpc/v1 {
|
|
if ($request_method = GET) {
|
|
return 303 /;
|
|
}
|
|
if ($request_method = HEAD) {
|
|
return 303 /;
|
|
}
|
|
|
|
limit_req zone=rpc burst=40 nodelay;
|
|
limit_req_status 429;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
# Limit request body to 1MB for RPC calls
|
|
client_max_body_size 1m;
|
|
|
|
# Increase timeout for long-running operations (e.g., Docker image pulls)
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Proxy API requests to backend
|
|
location /rpc/ {
|
|
limit_req zone=rpc burst=40 nodelay;
|
|
limit_req_status 429;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
# Limit request body to 1MB for RPC calls
|
|
client_max_body_size 1m;
|
|
|
|
# Increase timeout for long-running operations (e.g., Docker image pulls)
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Backend status endpoints (must be before the SPA catch-all)
|
|
location /health {
|
|
proxy_pass http://127.0.0.1:5678/health;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_connect_timeout 5s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
location /bitcoin-status {
|
|
proxy_pass http://127.0.0.1:5678/bitcoin-status;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
location /electrs-status {
|
|
proxy_pass http://127.0.0.1:5678/electrs-status;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Pine node status — live node facts for the Pine launcher page and the
|
|
# seeded Home Assistant sensors. Sensitive fields are token-gated at the
|
|
# backend; nginx only forwards.
|
|
location /api/pine/status {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header Authorization $http_authorization;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 15s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
location /lnd-connect-info {
|
|
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
add_header Access-Control-Allow-Origin $http_origin always;
|
|
add_header Access-Control-Allow-Credentials "true" always;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# LND REST proxy — backend handles auth + CORS
|
|
location /proxy/lnd/ {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Content sharing — peer access over Tor (no auth)
|
|
location /content {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 256m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 30s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Blob store — peer-facing download (HMAC capability auth, no session)
|
|
location /blob/ {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 64m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 60s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Blob store — local upload (session-authenticated, raw body)
|
|
location /api/blob {
|
|
client_max_body_size 64m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
proxy_request_buffering off;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header Cookie $http_cookie;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# App Store catalog proxy — backend fetches from configured registries
|
|
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
|
# through to the SPA index.html and the frontend gets HTML back instead
|
|
# of JSON.
|
|
location /api/app-catalog {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_connect_timeout 15s;
|
|
proxy_read_timeout 30s;
|
|
proxy_send_timeout 15s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# DWN endpoints — peer access over Tor (no auth)
|
|
location /dwn {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 256m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 30s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Proxy apps that set X-Frame-Options - strip header so iframe works
|
|
location /app/nextcloud/ {
|
|
proxy_pass http://127.0.0.1:8085/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/vaultwarden/ {
|
|
proxy_pass http://127.0.0.1:8082/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/immich/ {
|
|
proxy_pass http://127.0.0.1:2283/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/penpot/ {
|
|
proxy_pass http://127.0.0.1:9001/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
# Block path traversal attempts before they reach FileBrowser
|
|
location ~* /app/filebrowser/api/resources/.*/\.\. {
|
|
return 403;
|
|
}
|
|
location ~* /app/filebrowser/api/raw/.*/\.\. {
|
|
return 403;
|
|
}
|
|
location /app/filebrowser/ {
|
|
client_max_body_size 10G;
|
|
proxy_pass http://127.0.0.1:8083/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_request_buffering off;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/grafana/ {
|
|
proxy_pass http://127.0.0.1:3000/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/jellyfin/ {
|
|
proxy_pass http://127.0.0.1:8096/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location = /app/uptime-kuma/ {
|
|
return 302 /app/uptime-kuma/dashboard;
|
|
}
|
|
location /app/uptime-kuma/ {
|
|
proxy_pass http://127.0.0.1:3002/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Prefix /app/uptime-kuma;
|
|
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_redirect / /app/uptime-kuma/;
|
|
proxy_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/portainer/ {
|
|
proxy_pass http://127.0.0.1:9000/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
# Remaining apps (also available on HTTPS via snippet include)
|
|
location /app/searxng/ {
|
|
proxy_pass http://127.0.0.1:8888/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/indeedhub/_next/ {
|
|
proxy_pass http://127.0.0.1:7778/_next/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_cache_valid 200 30d;
|
|
add_header Cache-Control "public, max-age=2592000, immutable";
|
|
}
|
|
# IndeeHub WebSocket proxy
|
|
location /app/indeedhub/ws/ {
|
|
proxy_pass http://127.0.0.1:7778/ws/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_read_timeout 86400s;
|
|
}
|
|
location /app/indeedhub/ {
|
|
proxy_pass http://127.0.0.1:7778/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_types text/css application/javascript application/json;
|
|
sub_filter_once off;
|
|
sub_filter 'href="/' 'href="/app/indeedhub/';
|
|
sub_filter 'src="/' 'src="/app/indeedhub/';
|
|
sub_filter "href='/" "href='/app/indeedhub/";
|
|
sub_filter "src='/" "src='/app/indeedhub/";
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/botfights/api/ {
|
|
proxy_pass http://127.0.0.1:9100/api/;
|
|
proxy_http_version 1.1;
|
|
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 300s;
|
|
proxy_send_timeout 300s;
|
|
}
|
|
location /app/botfights/ {
|
|
proxy_pass http://127.0.0.1:9100/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
proxy_hide_header Cross-Origin-Embedder-Policy;
|
|
proxy_hide_header Cross-Origin-Opener-Policy;
|
|
proxy_hide_header Cross-Origin-Resource-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_types text/css application/javascript application/json;
|
|
sub_filter_once off;
|
|
sub_filter 'href="/' 'href="/app/botfights/';
|
|
sub_filter 'src="/' 'src="/app/botfights/';
|
|
sub_filter "href='/" "href='/app/botfights/";
|
|
sub_filter "src='/" "src='/app/botfights/";
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script><script>window.addEventListener("message",function(e){var d=e.data;if(d&&d.type==="arcade-input"&&d.key){var t=d.action==="up"?"keyup":"keydown";document.dispatchEvent(new KeyboardEvent(t,{key:d.key,bubbles:true}))}})</script></head>';
|
|
}
|
|
location /app/gitea/ {
|
|
proxy_pass http://127.0.0.1:3001/;
|
|
proxy_set_header Host $http_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;
|
|
client_max_body_size 1G;
|
|
proxy_hide_header X-Frame-Options;
|
|
proxy_hide_header Content-Security-Policy;
|
|
# Override parent add_header to allow iframe embedding
|
|
add_header X-Content-Type-Options nosniff always;
|
|
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
|
}
|
|
location /app/lnd/ {
|
|
proxy_pass http://127.0.0.1:18083/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/mempool/ {
|
|
proxy_pass http://127.0.0.1:4080/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/photoprism/ {
|
|
proxy_pass http://127.0.0.1:2342/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/fedimint/ {
|
|
proxy_pass http://127.0.0.1:8175/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_types text/css application/javascript application/json;
|
|
sub_filter_once off;
|
|
sub_filter 'href="/' 'href="/app/fedimint/';
|
|
sub_filter 'src="/' 'src="/app/fedimint/';
|
|
sub_filter "href='/" "href='/app/fedimint/";
|
|
sub_filter "src='/" "src='/app/fedimint/";
|
|
sub_filter 'url("/' 'url("/app/fedimint/';
|
|
sub_filter "url('/" "url('/app/fedimint/";
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/fedimint-gateway/ {
|
|
proxy_pass http://127.0.0.1:8176/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/tailscale/ {
|
|
# Tailscale has no web UI — managed via CLI/Tailscale app
|
|
default_type application/json;
|
|
return 503 '{"error":{"code":"NO_WEB_UI","message":"Tailscale is managed via CLI"}}';
|
|
}
|
|
location /app/routstr/ {
|
|
proxy_pass http://127.0.0.1:8200/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/nostr-vpn/ {
|
|
proxy_pass http://127.0.0.1:8201/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
}
|
|
location /app/fips/ {
|
|
proxy_pass http://127.0.0.1:8202/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
}
|
|
location /app/ollama/ {
|
|
proxy_pass http://127.0.0.1:11434/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/bitcoin-ui/ {
|
|
proxy_pass http://127.0.0.1:8334/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/electrumx/ {
|
|
proxy_pass http://127.0.0.1:50002/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/endurain/ {
|
|
proxy_pass http://127.0.0.1:8080/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/nginx-proxy-manager/ {
|
|
proxy_pass http://127.0.0.1:8081/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/btcpay/ {
|
|
proxy_pass http://127.0.0.1:23000/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Prefix /app/btcpay;
|
|
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_redirect http://127.0.0.1:23000/ /app/btcpay/;
|
|
proxy_redirect / /app/btcpay/;
|
|
proxy_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/homeassistant/ {
|
|
proxy_pass http://127.0.0.1:8123/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 86400s;
|
|
proxy_send_timeout 86400s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
|
|
# External site proxies — strip X-Frame-Options so iframe embedding works.
|
|
# add_header here prevents inheritance of server-level X-Frame-Options.
|
|
location /ext/nostrudel/ {
|
|
set $upstream_5 "https://nostrudel.ninja/";
|
|
|
|
proxy_pass $upstream_5;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host nostrudel.ninja;
|
|
proxy_set_header Accept-Encoding "";
|
|
proxy_ssl_server_name on;
|
|
proxy_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
proxy_hide_header Cross-Origin-Embedder-Policy;
|
|
proxy_hide_header Cross-Origin-Opener-Policy;
|
|
proxy_hide_header Cross-Origin-Resource-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
sub_filter_once off;
|
|
sub_filter_types text/css application/javascript;
|
|
sub_filter 'href="/' 'href="/ext/nostrudel/';
|
|
sub_filter 'src="/' 'src="/ext/nostrudel/';
|
|
sub_filter 'action="/' 'action="/ext/nostrudel/';
|
|
sub_filter "href='/" "href='/ext/nostrudel/";
|
|
sub_filter "src='/" "src='/ext/nostrudel/";
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
|
|
# Proxy WebSocket
|
|
location /ws {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_read_timeout 86400s;
|
|
}
|
|
}
|
|
|
|
# HTTPS - required for PWA install (Add to Home Screen) from dev servers
|
|
server {
|
|
listen 443 ssl default_server;
|
|
listen [::]:443 ssl default_server;
|
|
server_name _;
|
|
|
|
ssl_certificate /etc/archipelago/ssl/archipelago.crt;
|
|
ssl_certificate_key /etc/archipelago/ssl/archipelago.key;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
|
|
|
root /opt/archipelago/web-ui;
|
|
index index.html;
|
|
include snippets/archipelago-pwa.conf;
|
|
|
|
# Same CA download over HTTPS — see the note in the HTTP block above.
|
|
location = /ca.crt {
|
|
alias /etc/archipelago/ssl/ca-download.crt;
|
|
default_type application/x-x509-ca-cert;
|
|
add_header Content-Disposition 'attachment; filename="archipelago-node-ca.crt"';
|
|
}
|
|
|
|
# Security headers
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
add_header X-DNS-Prefetch-Control "off" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
|
|
|
# JSON error responses — prevents leaking HTML error pages to API clients
|
|
location @backend_unavailable {
|
|
default_type application/json;
|
|
return 502 '{"error":{"code":"BACKEND_UNAVAILABLE","message":"Service temporarily unavailable"}}';
|
|
}
|
|
location @backend_timeout {
|
|
default_type application/json;
|
|
return 504 '{"error":{"code":"BACKEND_TIMEOUT","message":"Service did not respond in time"}}';
|
|
}
|
|
|
|
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
|
|
location /aiui/ {
|
|
try_files $uri $uri/ /aiui/index.html;
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
}
|
|
location /aiui/api/claude/ {
|
|
proxy_pass http://127.0.0.1:3142/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_connect_timeout 120s;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
location /aiui/api/ollama/ {
|
|
proxy_pass http://127.0.0.1:11434/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_connect_timeout 120s;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
location /aiui/api/openrouter/ {
|
|
set $upstream_6 "https://openrouter.ai/api/";
|
|
|
|
proxy_pass $upstream_6;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host openrouter.ai;
|
|
proxy_ssl_server_name on;
|
|
proxy_connect_timeout 120s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
}
|
|
|
|
# Icons, favicon, manifest — always revalidate (no heuristic caching)
|
|
location ~* ^/(favicon\.ico|manifest\.webmanifest|assets/icon/) {
|
|
add_header Cache-Control "no-cache, must-revalidate";
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# Versioned Vite assets must never fall through to index.html (mirrors the
|
|
# HTTP block). No `always` on the immutable header: a transient 404 must
|
|
# not be cached for a year — 404s go to @asset_missing (no-store) instead.
|
|
location /assets/ {
|
|
try_files $uri @aiui_assets;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
}
|
|
# AIUI's built index.html emits ABSOLUTE /assets/<hashed> paths, so they
|
|
# land here, in the MAIN UI's assets dir, where they do not exist — the
|
|
# real files are in aiui/assets/. The nearby /aiui-assets/ alias names this
|
|
# exact problem in its comment but only catches a path AIUI never requests,
|
|
# so it never fixed it.
|
|
#
|
|
# A fallback rather than copying the files up one level: a frontend deploy
|
|
# replaces web-ui wholesale (update.rs preserves the aiui/ directory itself,
|
|
# not copies made into assets/), so a copy is erased by the very next deploy
|
|
# while this survives. Hash collision with a real main-UI asset is not a
|
|
# concern — Vite hashes are content-derived, and a main asset that exists is
|
|
# served by try_files before this is ever consulted.
|
|
location @aiui_assets {
|
|
rewrite ^/assets/(.*)$ /aiui/assets/$1 break;
|
|
try_files $uri =404;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
error_page 404 = @asset_missing;
|
|
}
|
|
location @asset_missing {
|
|
add_header Cache-Control "no-store" always;
|
|
return 404;
|
|
}
|
|
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
location /archipelago/ {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 256m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 30s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
location /health {
|
|
proxy_pass http://127.0.0.1:5678/health;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_connect_timeout 5s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
location /bitcoin-status {
|
|
proxy_pass http://127.0.0.1:5678/bitcoin-status;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
location /electrs-status {
|
|
proxy_pass http://127.0.0.1:5678/electrs-status;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Pine node status — live node facts for the Pine launcher page and the
|
|
# seeded Home Assistant sensors. Sensitive fields are token-gated at the
|
|
# backend; nginx only forwards.
|
|
location /api/pine/status {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header Authorization $http_authorization;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 15s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
location /lnd-connect-info {
|
|
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
add_header Access-Control-Allow-Origin $http_origin always;
|
|
add_header Access-Control-Allow-Credentials "true" always;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# LND REST proxy — backend handles auth + CORS
|
|
location /proxy/lnd/ {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 10s;
|
|
proxy_send_timeout 5s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Content sharing — peer access over Tor (no auth)
|
|
location /content {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 256m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 30s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Blob store — peer-facing download (HMAC capability auth, no session)
|
|
location /blob/ {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 64m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 60s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# Blob store — local upload (session-authenticated, raw body)
|
|
location /api/blob {
|
|
client_max_body_size 64m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 120s;
|
|
proxy_send_timeout 120s;
|
|
proxy_request_buffering off;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header Cookie $http_cookie;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# App Store catalog proxy — backend fetches from configured registries
|
|
# so the browser doesn't hit CORS/CSP. Without this block nginx falls
|
|
# through to the SPA index.html and the frontend gets HTML back instead
|
|
# of JSON.
|
|
location /api/app-catalog {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_connect_timeout 15s;
|
|
proxy_read_timeout 30s;
|
|
proxy_send_timeout 15s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
# DWN endpoints — peer access over Tor (no auth)
|
|
location /dwn {
|
|
limit_req zone=peer burst=20 nodelay;
|
|
client_max_body_size 256m;
|
|
proxy_connect_timeout 30s;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 30s;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
location /rpc/ {
|
|
limit_req zone=rpc burst=40 nodelay;
|
|
limit_req_status 429;
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
# Limit request body to 1MB for RPC calls
|
|
client_max_body_size 1m;
|
|
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
error_page 502 503 = @backend_unavailable;
|
|
error_page 504 = @backend_timeout;
|
|
}
|
|
|
|
location /app/nextcloud/ {
|
|
proxy_pass http://127.0.0.1:8085/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/vaultwarden/ {
|
|
proxy_pass http://127.0.0.1:8082/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/immich/ {
|
|
proxy_pass http://127.0.0.1:2283/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/penpot/ {
|
|
proxy_pass http://127.0.0.1:9001/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/btcpay/ {
|
|
proxy_pass http://127.0.0.1:23000/;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Prefix /app/btcpay;
|
|
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_redirect http://127.0.0.1:23000/ /app/btcpay/;
|
|
proxy_redirect / /app/btcpay/;
|
|
proxy_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
location /app/homeassistant/ {
|
|
proxy_pass http://127.0.0.1:8123/;
|
|
proxy_http_version 1.1;
|
|
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_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
proxy_read_timeout 86400s;
|
|
proxy_send_timeout 86400s;
|
|
proxy_set_header Accept-Encoding "";
|
|
sub_filter_once on;
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
# All remaining app proxies (mempool, fedimint, lnd, bitcoin-ui, etc.)
|
|
include snippets/archipelago-https-app-proxies.conf;
|
|
|
|
# External site proxies — strip X-Frame-Options so iframe embedding works.
|
|
# add_header here prevents inheritance of server-level X-Frame-Options.
|
|
location /ext/nostrudel/ {
|
|
set $upstream_10 "https://nostrudel.ninja/";
|
|
|
|
proxy_pass $upstream_10;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host nostrudel.ninja;
|
|
proxy_set_header Accept-Encoding "";
|
|
proxy_ssl_server_name on;
|
|
proxy_hide_header X-Frame-Options;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
proxy_hide_header Content-Security-Policy;
|
|
proxy_hide_header Cross-Origin-Embedder-Policy;
|
|
proxy_hide_header Cross-Origin-Opener-Policy;
|
|
proxy_hide_header Cross-Origin-Resource-Policy;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
sub_filter_once off;
|
|
sub_filter_types text/css application/javascript;
|
|
sub_filter 'href="/' 'href="/ext/nostrudel/';
|
|
sub_filter 'src="/' 'src="/ext/nostrudel/';
|
|
sub_filter 'action="/' 'action="/ext/nostrudel/';
|
|
sub_filter "href='/" "href='/ext/nostrudel/";
|
|
sub_filter "src='/" "src='/ext/nostrudel/";
|
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
|
}
|
|
|
|
location /ws {
|
|
proxy_pass http://127.0.0.1:5678;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header Cookie $http_cookie;
|
|
proxy_read_timeout 86400s;
|
|
}
|
|
}
|
|
|