{ config, pkgs, lib, ... }: let # NOTE: API keys will be loaded from /srv/nginx/secrets at runtime # This file should contain one Bearer token per line # The secrets file is read at runtime via include directive instead of build time # to avoid flake purity issues in { services.nginx = { enable = true; # Recommended settings recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; # Increase bucket size for long Bearer tokens mapHashBucketSize = 128; # Map directive to check Authorization header against multiple keys # Keys are loaded from /srv/nginx/secrets.map at runtime appendHttpConfig = '' # Check if the Authorization header matches any expected value map $http_authorization $auth_status { default "unauthorized"; "" "no_auth"; # Tokens loaded from file to keep secrets out of the nix store # Format: "Bearer YOUR_TOKEN_HERE" "authorized"; include /srv/nginx/secrets.map; } ''; # Virtual hosts configuration virtualHosts = { # Main website - Static HTML/CSS "www.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; root = "/srv/www/binning.net"; locations."/" = { index = "index.html"; tryFiles = "$uri $uri/ =404"; extraConfig = '' # Enable Server Side Includes for navbar/footer includes ssi on; ''; }; # Private blog articles with HTTP basic authentication locations."/blog/private/" = { extraConfig = '' auth_basic "Private Articles"; auth_basic_user_file "/srv/nginx/.htpasswd"; # Enable Server Side Includes ssi on; ''; }; # Optional: Custom 404 page extraConfig = '' error_page 404 /404.html; ''; }; # Ollama with Bearer token authentication "ollama.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { extraConfig = '' # Check auth status if ($auth_status = "no_auth") { return 401 "Unauthorized: Bearer token required\n"; } if ($auth_status = "unauthorized") { return 403 "Forbidden: Invalid API key\n"; } # Proxy to Ollama (only if authorized) proxy_pass http://localhost: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_set_header Connection ""; # Disable buffering for streaming (SSE) responses proxy_buffering off; # Timeouts for long-running requests proxy_read_timeout 300s; proxy_connect_timeout 300s; proxy_send_timeout 300s; # Allow large request bodies client_max_body_size 100M; # Logging access_log /var/log/nginx/ollama_access.log; error_log /var/log/nginx/ollama_error.log; ''; }; }; # LM Studio with Bearer token authentication # Proxies https://lmstudio.binning.net/v1 to http://localhost:1234/v1. "lmstudio.binning.net" = { forceSSL = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { extraConfig = '' # Check auth status if ($auth_status = "no_auth") { return 401 "Unauthorized: Bearer token required\n"; } if ($auth_status = "unauthorized") { return 403 "Forbidden: Invalid API key\n"; } # Proxy to LM Studio (running on port 1234) # Note: The trailing slash is important - it preserves the /v1 path proxy_pass http://localhost:1234/; 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_set_header Connection ""; # Disable buffering for streaming (SSE) responses proxy_buffering off; # Timeouts for long-running requests proxy_read_timeout 300s; proxy_connect_timeout 300s; proxy_send_timeout 300s; # Allow large request bodies client_max_body_size 100M; # Logging access_log /var/log/nginx/lmstudio_access.log; error_log /var/log/nginx/lmstudio_error.log; ''; }; }; # Forgejo "forgejo.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:3000"; # No extraConfig needed - recommendedProxySettings handles headers }; }; # Radicale "radicale.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:5232"; # recommendedProxySettings handles most headers extraConfig = '' proxy_set_header X-Script-Name ""; ''; }; }; # DocuSeal "docuseal.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:3030"; proxyWebsockets = true; }; }; # ComfyUI with HTTP basic authentication "comfyui.binning.net" = { forceSSL = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:8188"; proxyWebsockets = true; extraConfig = '' auth_basic "ComfyUI"; auth_basic_user_file "/srv/nginx/.htpasswd"; ''; }; }; # RustDesk "rustdesk.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:16484"; proxyWebsockets = true; }; }; }; }; # Firewall networking.firewall.allowedTCPPorts = [ 80 443 ]; # ACME/Let's Encrypt security.acme = { acceptTerms = true; defaults.email = "hvanb@pm.me"; }; # Ensure the data directory exists with proper permissions systemd.tmpfiles.rules = [ "d /var/lib/www 0750 nginx nginx -" ]; }