Files
nixos/modules/server/nginx.nix
T
2026-10-08 04:32:23 +03:00

325 lines
9.4 KiB
Nix

{
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" ''
<!doctype html>
<html>
<body>
<pre>What are you doing here?</pre>
</body>
</html>
'';
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.
}