{ config, lib, pkgs, xlib, ... }: # Standard reverse-proxy: HTTP/S termination upstream, backend on the LAN. # x.zeroq.su is the 3x-ui controller panel — see 3x-ui.nix for the # /subs/, /subsjs/, /clash/ routing logic. let server = "192.168.1.20"; mkProxy = { domain, port, addSSL ? false, extraConfig ? "", }: { name = domain; value = { enableACME = true; locations."/" = { proxyPass = "http://${server}:${toString port}"; proxyWebsockets = true; }; } // lib.optionalAttrs (!addSSL) { forceSSL = true; } // lib.optionalAttrs addSSL { addSSL = true; } // lib.optionalAttrs (extraConfig != "") { inherit extraConfig; }; }; bigUploads = "client_max_body_size 5G;"; sites = [ { domain = "immich.zeroq.su"; port = 2283; addSSL = true; extraConfig = bigUploads; } { domain = "kuma.zeroq.su"; port = 4001; } { domain = "health.zeroq.su"; port = 19999; } { domain = "git.zeroq.su"; port = 3000; } { domain = "homebox.zeroq.su"; port = 7745; } { domain = "flux.zeroq.su"; port = 6061; } { domain = "tape-rotation.zeroq.su"; port = 5174; } # NOTE: open.zeroq.su is intentionally NOT in this `sites` list — # mkProxy hard-codes ${server} = 192.168.1.20, but the Open WebUI # container binds to 127.0.0.1:8080 only (loopback, see # modules/containers/open-webui.nix). The vhost is added directly # to `virtualHosts` below, alongside x.zeroq.su (3x-ui panel, # same loopback-only pattern). { domain = "navidrome.zeroq.su"; port = 4533; addSSL = true; } { domain = "calibre.zeroq.su"; port = 8083; extraConfig = bigUploads; } { domain = "nix-cache.zeroq.su"; port = 5000; extraConfig = bigUploads; } { domain = "pdf.zeroq.su"; port = 8446; extraConfig = bigUploads; } ]; in { services.nginx = { enable = true; recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // { "nextcloud.private" = { forceSSL = false; enableACME = false; listen = [ { addr = "100.64.0.0"; port = 10000; } { addr = "192.168.1.20"; port = 10000; } { addr = "127.0.0.1"; port = 10000; } ]; }; "office.zeroq.su" = { forceSSL = true; enableACME = true; }; # vtimeline.zeroq.su — static site behind Authelia forward-auth. # Files live under /home/oqyude/External/Git/VeeamTimelineView/public_html, # which is bind-mounted to /var/lib/vtimeline (see systemd.mounts below) # because /home/oqyude is mode 700 and the nginx user (uid 60) cannot # traverse it. Authentication is delegated to Authelia via # auth_request: nginx sub-requests /authelia on every hit, Authelia # returns 2xx if the session cookie is valid or 401 (which nginx # converts into a 401 to the client; Authelia's response headers # carry the redirect target). The login UI itself is served by the # authelia.zeroq.su vhost below — same Authelia container, different # vhost. "vtimeline.zeroq.su" = { forceSSL = true; enableACME = true; root = "/var/lib/vtimeline"; locations = { "/" = { extraConfig = '' auth_request /authelia; auth_request_set $authelia_user $upstream_http_remote_user; ''; }; "= /authelia" = { extraConfig = '' internal; proxy_pass http://127.0.0.1:9091/api/authz/forward-auth; proxy_set_header X-Original-URL $request_uri; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Method $request_method; proxy_set_header X-Forwarded-Uri $request_uri; proxy_set_header X-Forwarded-For $remote_addr; ''; }; }; }; # Authelia login UI — same podman container on 127.0.0.1:9091 as the # forward-auth endpoint above, just exposed on a separate vhost so # Authelia has a stable absolute URL to redirect users to. Authelia # generates internal links against $session.cookies[0].authelia_url, # which is set to https://${autheliaFqdn}/ in modules/server/authelia.nix. "authelia.zeroq.su" = { forceSSL = true; enableACME = true; locations."/" = { proxyPass = "http://127.0.0.1:9091"; proxyWebsockets = true; }; }; "pdf.private" = { forceSSL = false; enableACME = false; listen = [ { addr = "0.0.0.0"; port = 80; } { addr = "100.64.0.0"; port = 8446; } { addr = "192.168.1.20"; port = 8446; } { addr = "127.0.0.1"; port = 8446; } ]; extraConfig = bigUploads; }; "x.zeroq.su" = { forceSSL = true; enableACME = true; locations = { "/" = { proxyPass = "http://127.0.0.1:2049"; proxyWebsockets = true; }; "/subs/" = { proxyPass = "http://127.0.0.1:2096"; proxyWebsockets = true; }; "/subsjs/" = { proxyPass = "http://127.0.0.1:2096"; proxyWebsockets = true; }; "/clash/" = { proxyPass = "http://127.0.0.1:2096"; proxyWebsockets = true; }; }; }; # Open WebUI — same loopback-only pattern as x.zeroq.su above. # The container listens on 127.0.0.1:8080 (modules/containers/open-webui.nix), # so we proxy_pass to 127.0.0.1, not the LAN IP. The two extra # directives are required by the upstream HTTPS docs: # proxy_buffering off for SSE streaming (markdown in chat breaks # under the default `proxy_buffering on` from recommendedProxySettings), # and a 300 s read timeout for long LLM completions. "open.zeroq.su" = { forceSSL = true; enableACME = true; locations."/" = { proxyPass = "http://127.0.0.1:8080"; proxyWebsockets = true; }; extraConfig = '' proxy_buffering off; proxy_read_timeout 300s; ''; }; "zeroq.su" = { forceSSL = true; enableACME = true; root = pkgs.writeTextDir "index.html" ''
What are you doing here?
''; locations."/guest/" = { proxyPass = "http://${server}:80"; proxyWebsockets = true; }; }; "vetymae.opencodes.zeroq.su" = { forceSSL = true; enableACME = true; locations."/" = { proxyPass = "http://100.86.62.4:4096"; proxyWebsockets = true; }; }; "lamet.opencodes.zeroq.su" = { forceSSL = true; enableACME = true; locations."/" = { proxyPass = "http://100.106.21.39:6061"; proxyWebsockets = true; }; }; # sapphira itself: opencode web runs as a systemd user service # (programs.opencode.web.enable in home/modules/opencode.nix) on # 127.0.0.1:4096 with --hostname 0.0.0.0. "opencode.zeroq.su" = { forceSSL = true; enableACME = true; locations."/" = { proxyPass = "http://127.0.0.1:4096"; proxyWebsockets = true; }; }; "nextcloud.zeroq.su" = { forceSSL = true; enableACME = true; locations = { "/" = { proxyPass = "http://${server}:10000"; proxyWebsockets = true; }; "/whiteboard" = { proxyPass = "http://${server}:3002"; proxyWebsockets = true; }; }; extraConfig = bigUploads; }; }; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; # Bind-mount the vtimeline source tree into /var/lib so the nginx user # (uid 60) doesn't have to traverse /home/oqyude (mode 700). The mount is # lazy (x-systemd.automount) and nofail, so a missing /home/oqyude/External # only shows up as a per-request 500/403, never as a hard boot failure. systemd.mounts = [ (xlib.helpers.mkSystemdBind { what = "/home/oqyude/External/Git/VeeamTimelineView/public_html"; where = "/var/lib/vtimeline"; }) ]; systemd.tmpfiles.rules = [ (xlib.helpers.mkTmpfile "d" "/var/lib/vtimeline" "0755" "nginx" "nginx") ]; # Note: the previous vtimeline-htpasswd sops declaration lived here. It # was removed when authelia replaced nginx's auth_basic (see the vtimeline # vhost above). The encrypted file modules/server/secrets/vtimeline-htpasswd.yaml # itself was kept untouched per the repo policy of not modifying secrets # without explicit owner sign-off; delete it with `sops --version` and # `rm` once the cutover is verified. }