Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:49 +00:00
commit a3b09fa2cd
1560 changed files with 331939 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Stable symlink for USB serial adapters used as mesh radios.
# Creates /dev/mesh-radio pointing to the underlying ttyUSB device.
# Supports MeshCore and Meshtastic radios using CP2102 (Heltec V3),
# CH340 (T-Beam), FTDI (RAK WisBlock), and known USB CDC ACM radios.
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", KERNEL=="ttyACM[0-9]*", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="2e8a", KERNEL=="ttyACM[0-9]*", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
@@ -0,0 +1,12 @@
[Unit]
Description=Archipelago Container Doctor
After=archipelago.service
[Service]
Type=oneshot
# Runs as root: needs to kill orphaned conmon processes, fix permissions
User=root
ExecStart=/home/archipelago/archy/scripts/container-doctor.sh --local
TimeoutStartSec=300
StandardOutput=journal
StandardError=journal
@@ -0,0 +1,14 @@
[Unit]
Description=Archipelago container doctor (periodic)
[Timer]
# First run 2 minutes after boot, then every 5 minutes. The doctor is
# idempotent and exits quickly when no drift exists; this keeps vanished
# rootless port listeners and stopped containers from remaining broken.
OnBootSec=2min
OnUnitActiveSec=5min
# Jitter to avoid load spikes
RandomizedDelaySec=60
[Install]
WantedBy=timers.target
@@ -0,0 +1,21 @@
[Unit]
Description=Archipelago FIPS mesh transport (wraps upstream fips daemon)
# Stay dark until onboarding materialises the seed-derived key. Archipelago
# backend unmasks + starts this unit via `sudo systemctl` once the key is
# present; pre-onboarding the unit must be masked so no traffic is sent
# from an ephemeral identity.
ConditionPathExists=/var/lib/archipelago/identity/fips_key
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStartPre=/bin/sh -c 'test -x /usr/bin/fips || { echo "fips daemon not installed — run fips.install from dashboard" >&2; exit 1; }'
ExecStart=/usr/bin/fips --config /etc/fips/fips.yaml
Restart=on-failure
RestartSec=5
# UDP 8668 is reachable on all interfaces by default; the daemon does its
# own Noise authentication so no firewall gate is added here.
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,143 @@
#!/bin/bash
# Start a dedicated X server for the attached kiosk display.
/usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty &
XPID=$!
export DISPLAY=:0
export HOME=/home/archipelago
X_READY=false
for _ in $(seq 1 30); do
if kill -0 "$XPID" 2>/dev/null && xrandr --query >/tmp/archipelago-kiosk-xrandr.txt 2>/dev/null; then
X_READY=true
break
fi
sleep 0.5
done
if [ "$X_READY" != "true" ]; then
echo 'ERROR: Xorg failed to become ready'
kill "$XPID" 2>/dev/null || true
exit 1
fi
KIOSK_SAFE_AREA_X_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_X_PX:-}
KIOSK_SAFE_AREA_Y_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_Y_PX:-}
# Actual mode of the kiosk output — configure_display overwrites these so
# Chromium's window matches the panel instead of assuming 1080p (a 1366x768
# laptop panel otherwise gets a clipped oversized window).
KIOSK_MODE_W=1920
KIOSK_MODE_H=1080
configure_display() {
command -v xrandr >/dev/null 2>&1 || return 0
local output mode internal width height
output=$(awk '/ connected/ && $1 !~ /^eDP|^LVDS/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt)
[ -n "$output" ] || output=$(awk '/ connected/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt)
[ -n "$output" ] || return 0
# Pick the EDID-preferred ("+") mode, falling back to the first-listed
# mode (EDID lists native first). Deliberately ignore "*" (currently
# active) — trusting "active" lets a bad clone/mirror state from a
# previous boot perpetuate itself forever instead of self-healing.
mode=$(awk -v out="$output" '
$1 == out { active = 1; next }
active && /^[[:space:]]+[0-9]+x[0-9]+/ {
if ($0 ~ /\+/ && !preferred) { preferred = $1 }
if (!first) first = $1
}
active && /^[^[:space:]]/ { active = 0 }
END { if (preferred) print preferred; else if (first) print first }
' /tmp/archipelago-kiosk-xrandr.txt)
[ -n "$mode" ] || mode=1920x1080
# Kiosk should use one native output. A spanning desktop makes Chromium land
# on the laptop panel or stretch across both outputs.
for internal in $(awk '/ connected/ && $1 ~ /^eDP|^LVDS/{print $1}' /tmp/archipelago-kiosk-xrandr.txt); do
[ "$internal" = "$output" ] || xrandr --output "$internal" --off 2>/dev/null || true
done
xrandr --output "$output" \
--primary \
--mode "$mode" \
--pos 0x0 \
--scale 1x1 \
--panning 0x0 \
--transform none 2>/dev/null || true
width=${mode%x*}
height=${mode#*x}
case "$width:$height" in *[!0-9:]*|:*) width=1920; height=1080 ;; esac
KIOSK_MODE_W=$width
KIOSK_MODE_H=$height
# Browser safe-area fallback for TVs that crop edges. Driver underscan is
# preferable, but many Intel HDMI outputs do not expose that property.
KIOSK_SAFE_AREA_X_PX=${KIOSK_SAFE_AREA_X_PX:-$((width * 3 / 100))}
KIOSK_SAFE_AREA_Y_PX=${KIOSK_SAFE_AREA_Y_PX:-$((height * 3 / 100))}
}
configure_display
xhost +SI:localuser:archipelago 2>/dev/null || true
xsetroot -solid black 2>/dev/null || true
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true
sleep 1
# GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization
# spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500)
# because under X11 it falls back to software compositing anyway — that CPU
# starvation is what caused choppy HDMI audio. --in-process-gpu avoids the
# separate process; GpuRasterization is also disabled via --disable-features below.
# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead.
if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then
GPU_FLAGS="--in-process-gpu --num-raster-threads=1"
else
GPU_FLAGS="--disable-gpu --num-raster-threads=1"
fi
ARCHIPELAGO_UID=$(id -u archipelago)
while true; do
# XDG_RUNTIME_DIR must be passed explicitly — without it Chromium's audio
# backend can't find PipeWire-Pulse's socket at /run/user/<uid>/pulse/native,
# falls back to raw ALSA "default", fails to connect, and produces no audio
# at all with no visible error (--noerrdialogs suppresses it).
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago XDG_RUNTIME_DIR=/run/user/$ARCHIPELAGO_UID chromium --kiosk \
--app=http://localhost/kiosk?safe_area_x=${KIOSK_SAFE_AREA_X_PX:-0}\&safe_area_y=${KIOSK_SAFE_AREA_Y_PX:-0} \
--noerrdialogs \
--disable-infobars \
--disable-translate \
--no-first-run \
--check-for-update-interval=31536000 \
--disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \
--disable-session-crashed-bubble \
--disable-save-password-bubble \
--disable-suggestions-service \
--disable-component-update \
$GPU_FLAGS \
--renderer-process-limit=2 \
--window-size=${KIOSK_MODE_W},${KIOSK_MODE_H} \
--window-position=0,0 \
--start-fullscreen \
--force-device-scale-factor=1 \
--disable-background-networking \
--disable-background-timer-throttling \
--disable-backgrounding-occluded-windows \
--disable-breakpad \
--disable-metrics \
--disable-metrics-reporting \
--disable-domain-reliability \
--js-flags="--max-old-space-size=128" \
--user-data-dir=/var/lib/archipelago/chromium-kiosk
sleep 3
done
kill "$XPID" 2>/dev/null || true
@@ -0,0 +1,14 @@
[Unit]
Description=Archipelago Kiosk Watchdog
After=archipelago.service
Wants=archipelago.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/archipelago-kiosk-watchdog
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,48 @@
[Unit]
Description=Archipelago Kiosk (X11 + Chromium)
After=archipelago.service systemd-user-sessions.service network-online.target
Wants=archipelago.service network-online.target
ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
Conflicts=getty@tty1.service
[Service]
Type=simple
# Wait up to 5 min for archipelago to serve /health. On slow hardware
# first-boot is dominated by the FileBrowser pull (unbundled ISO),
# initial archipelago state sync, and frontend settle — .198 took
# longer than 120s and chromium launched against an empty backend,
# producing a white window that only recovered on reboot. 300s gives
# slow-but-functional hardware enough headroom; TimeoutStartSec is
# bumped in lockstep so systemd doesn't kill us mid-wait.
ExecStartPre=/bin/bash -c 'for i in $(seq 1 150); do curl -sf http://localhost/health >/dev/null 2>&1 && break; sleep 2; done'
# Also wait for the web-ui asset swap to finish. The first-boot rsync into
# /opt/archipelago/web-ui is non-atomic and writes the large bg-*.webp images
# last — a kiosk launched mid-swap rendered the UI with blank backgrounds
# (CSS background-image 404s are never refetched). A representative large
# asset answering 200 means the swap is effectively done. Bounded 60s so a
# renamed asset can never block kiosk startup.
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf -o /dev/null http://localhost/assets/img/bg-home.webp && break; sleep 2; done; exit 0'
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
TimeoutStartSec=360
Restart=always
RestartSec=5
# Resource guardrail (#36). On GPU-less / headless hardware chromium could spin
# software compositing at ~92% of a core, saturating the node and starving the
# backend (it caused the .198 receive timeout + deploy storms). Cap CPU + memory
# so a runaway kiosk can never take the whole machine down; Delegate so the cap
# also binds the chromium/Xorg children in this unit's cgroup.
# CPUQuota=75% (0.75 cores) was too tight even for normal playback — the kiosk
# was throttled ~40% of the time, which is what caused choppy HDMI audio on
# archy-x250-exp (2026-06-28 incident). 200% (2 cores) gives enough headroom.
Delegate=yes
CPUQuota=200%
# Raised from 1500M/1200M: a Framework (Tiger Lake) kiosk sat at 806M used /
# 1.1G peak, riding the old MemoryHigh reclaim-throttle line — the throttling
# itself was the perceived UI lag. Keep Max well above real peaks; High stays
# the soft reclaim line so a runaway kiosk still can't take the machine down.
MemoryMax=2800M
MemoryHigh=2200M
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,10 @@
# Archipelago persistent log directory and files
# Runtime log destination. Backend runs as `archipelago`, but /var/log/
# is root-owned, so we pre-create the directory and log files with the
# right ownership at boot / install-time.
#
# Logrotate (image-recipe/configs/logrotate.conf) rotates files in this
# directory daily, keeping 30 compressed copies.
d /var/log/archipelago 0755 archipelago archipelago - -
f /var/log/archipelago/container-installs.log 0644 archipelago archipelago - -
@@ -0,0 +1,9 @@
[Unit]
Description=Watch for Archipelago Tor management actions
[Path]
PathExists=/var/lib/archipelago/tor-config/tor-action
MakeDirectory=yes
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,9 @@
[Unit]
Description=Process Archipelago Tor management action
After=tor.service
[Service]
Type=oneshot
ExecStart=/opt/archipelago/scripts/tor-helper.sh
# Runs as root — needs to write /etc/tor/torrc and restart tor.service
User=root
@@ -0,0 +1,19 @@
[Unit]
Description=Archipelago Self-Update
After=network-online.target archipelago.service
Wants=network-online.target
ConditionPathExists=/home/archipelago/archy/.git
[Service]
Type=oneshot
User=archipelago
ExecStart=/home/archipelago/archy/scripts/self-update.sh
TimeoutStartSec=600
Environment="HOME=/home/archipelago"
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/archipelago/.cargo/bin"
# Allow sudo for service restart and file install
# Requires archipelago user in sudoers for specific commands
StandardOutput=journal
StandardError=journal
@@ -0,0 +1,14 @@
[Unit]
Description=Check for Archipelago updates daily
ConditionPathExists=/home/archipelago/archy/.git
[Timer]
# Check at 3 AM daily (low-activity window)
OnCalendar=*-*-* 03:00:00
# Randomize within 30 min window to avoid thundering herd
RandomizedDelaySec=1800
# Run once on boot if last check was missed
Persistent=true
[Install]
WantedBy=timers.target
@@ -0,0 +1,14 @@
[Unit]
Description=Assign WireGuard server address to wg0
After=archipelago-wg.service
Wants=archipelago-wg.service
ConditionPathExists=/sys/class/net/wg0
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -c 'ip address show dev wg0 | grep -q "10.44.0.1" || ip address add 10.44.0.1/16 dev wg0'
ExecStart=/bin/bash -c 'iptables -t nat -C POSTROUTING -s 10.44.0.0/16 ! -o wg0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.44.0.0/16 ! -o wg0 -j MASQUERADE'
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,13 @@
[Unit]
Description=Archipelago Standalone WireGuard (wg0)
After=network.target
ConditionPathExists=/var/lib/archipelago/wireguard/private.key
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/archipelago-wg setup /var/lib/archipelago/wireguard/private.key
ExecStop=/bin/bash -c 'ip link del wg0 2>/dev/null || true'
[Install]
WantedBy=multi-user.target
+74
View File
@@ -0,0 +1,74 @@
[Unit]
Description=Archipelago Backend
After=network-online.target archipelago-setup-tor.service
Wants=network-online.target
# The data dir AND podman's graphroot (containers/storage) both live on the
# separate /var/lib/archipelago volume. Without this, on a cold boot the service
# (and its ExecStartPre) can start BEFORE var-lib-archipelago.mount, write to the
# bare mountpoint on rootfs, fail every podman call, exit, and get restarted every
# 5s until the volume mounts (~5 min of "[FAILED] Failed to start" on boot — B17).
# RequiresMountsFor adds both Requires= and After= on the mount unit so we never
# start until the data volume is mounted.
RequiresMountsFor=/var/lib/archipelago
[Service]
Type=notify
User=archipelago
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
Environment="ARCHIPELAGO_USE_QUADLET_BACKENDS=true"
EnvironmentFile=-/var/lib/archipelago/telemetry.env
# DEV_MODE disabled in production — enabled via override.conf on dev servers
Environment="XDG_RUNTIME_DIR=/run/user/1000"
# + prefix runs these as root (needed for chown/mkdir outside ReadWritePaths)
ExecStartPre=+/bin/bash -c 'mkdir -p /run/user/1000 /var/lib/containers && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
ExecStartPre=+/bin/bash -c 'mkdir -p /var/lib/archipelago && chown archipelago:archipelago /var/lib/archipelago && echo "ARCHIPELAGO_HOST_IP=$(hostname -I 2>/dev/null | awk "{print $$1}")" > /var/lib/archipelago/host-ip.env && chown archipelago:archipelago /var/lib/archipelago/host-ip.env'
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
RestartSec=5
WatchdogSec=300
TimeoutStartSec=300
# Backend shuts down in <1s; 15s is generous for any cleanup
TimeoutStopSec=15
# Filesystem protection
ProtectSystem=strict
# ProtectHome=no: rootless podman needs writable ~/.local/share/containers
ProtectHome=no
# PrivateTmp disabled: rootless podman runtime lives in /tmp/podman-run-UID/
# and must be shared between the service and SSH-created containers
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/user /tmp /home/archipelago/.local/share/containers /home/archipelago/.config/containers /etc
# Privilege restriction — NoNewPrivileges=no required for sudo archipelago-wg
# (WireGuard peer management). Scoped via sudoers to only archipelago-wg.
NoNewPrivileges=no
PrivateDevices=no
SupplementaryGroups=dialout debian-tor fips
# Syscall and network restrictions — safe on Debian 13 (systemd 256+)
# which respects NoNewPrivileges=no as an explicit override for seccomp filters
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictRealtime=yes
# MemoryDenyWriteExecute removed: ring (rustls) and secp256k1 (bitcoin/nostr)
# use assembly code that requires executable memory mappings on some platforms
# Resource limits
MemoryMax=4G
LimitNOFILE=65535
TasksMax=2048
# Delegate cgroup controllers so rootless podman (run from this system service
# as user=archipelago, not user@1000.service) can create transient libpod-*.scope
# units with --memory / --cpus / --pids-limit. Without this, podman create fails
# at start time with: "MemoryMax is out of range" because systemd rejects resource
# limits on undelegated cgroup subtrees. Required for the ProdContainerOrchestrator
# code path (see core/archipelago/src/container/prod_orchestrator.rs).
Delegate=memory pids cpu io
# Logging
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+7
View File
@@ -0,0 +1,7 @@
pcm.!default {
type pulse
hint.description "Default ALSA Device (via PulseAudio)"
}
ctl.!default {
type pulse
}
@@ -0,0 +1,7 @@
[Journal]
Storage=persistent
SystemMaxUse=500M
RuntimeMaxUse=100M
ForwardToSyslog=no
RateLimitIntervalSec=30s
RateLimitBurst=10000
+24
View File
@@ -0,0 +1,24 @@
# Log rotation configuration for Archipelago
/var/log/archipelago/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0644 root root
sharedscripts
postrotate
/usr/bin/systemctl reload archipelago > /dev/null 2>&1 || true
endscript
}
/var/lib/archipelago/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0644 archipelago archipelago
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
# Gitea iframe proxy — strips X-Frame-Options so Gitea works in Archipelago iframe.
# Gitea container binds to port 3001, this proxy listens on port 3000 (the public port).
# Deployed to /etc/nginx/conf.d/gitea-iframe.conf
server {
listen 3000;
server_name _;
client_max_body_size 1G;
location / {
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
}
@@ -0,0 +1,19 @@
[info]
relay_url = "ws://0.0.0.0:7777/"
name = "Archipelago Private Relay"
description = "Private Nostr relay for Archipelago mesh VPN peer discovery"
[database]
data_directory = "/var/lib/archipelago/nostr-relay"
in_memory = true
[network]
address = "0.0.0.0"
port = 7777
ping_interval = 120
[limits]
messages_per_sec = 50
max_event_bytes = 65536
max_ws_message_bytes = 65536
max_ws_frame_bytes = 65536
+24
View File
@@ -0,0 +1,24 @@
[Unit]
Description=Archipelago Private Nostr Relay
After=network-online.target
Wants=network-online.target
Before=nostr-vpn.service
[Service]
Type=simple
User=archipelago
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago/nostr-relay'
ExecStart=/usr/local/bin/nostr-rs-relay --config /var/lib/archipelago/nostr-relay/config.toml
Restart=always
RestartSec=3
TimeoutStopSec=10
# Resource limits — relay is lightweight (in-memory mode)
MemoryMax=512M
LimitNOFILE=4096
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+32
View File
@@ -0,0 +1,32 @@
[Unit]
Description=Nostr VPN - Mesh VPN with Nostr identity
After=network-online.target tor.service archipelago.service
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=10
[Service]
Type=simple
User=root
Environment=HOME=/var/lib/archipelago/nostr-vpn
EnvironmentFile=-/var/lib/archipelago/nostr-vpn/env
ExecStartPre=+/bin/bash -c 'mkdir -p /run/nostr-vpn /var/lib/archipelago/nostr-vpn/.config/nvpn'
ExecStartPre=/bin/bash -c 'test -f /var/lib/archipelago/nostr-vpn/env || { echo "NostrVPN not configured — waiting for onboarding"; exit 1; }'
ExecStart=/usr/local/bin/nvpn daemon
Restart=on-failure
RestartSec=30
TimeoutStartSec=30
TimeoutStopSec=10
# No sandbox — runs as root for TUN/WireGuard, needs unrestricted filesystem
# Resource limits
MemoryMax=256M
TasksMax=64
# Logging
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,366 @@
# App proxies for HTTPS - avoids mixed content when embedding apps from HTTPS page
# Complete list for all apps that may be launched from the UI
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;
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;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/gitea/ {
proxy_pass http://127.0.0.1:3001/;
proxy_http_version 1.1;
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;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
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;
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;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
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;
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/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;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
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;
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/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;
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;
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;
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/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;
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;
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;
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;
}
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;
}
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;
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;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
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;
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/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;
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:7777/_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";
}
location /app/indeedhub/ws/ {
proxy_pass http://127.0.0.1:7777/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:7777/;
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_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/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;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
@@ -0,0 +1,16 @@
# PWA installability - required for Install (not just Add to Home Screen) on Android
# Manifest MUST be served with application/manifest+json - Chrome rejects otherwise
location = /manifest.webmanifest {
default_type application/manifest+json;
add_header Cache-Control "public, max-age=0, must-revalidate";
}
# Service worker - no cache so updates apply
location ~ ^/(sw\.js|workbox-.*\.js|registerSW\.js)$ {
add_header Content-Type application/javascript;
add_header Service-Worker-Allowed /;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# index.html - avoid aggressive cache for PWA updates
location = /index.html {
add_header Cache-Control "public, max-age=0, must-revalidate";
}