blog/prod.nginx.nix
Matthew Binning 5b25211618 feat: Add deployment to a staging environment
Added staging.nginx.nix modeled on prod.nginx.nix, configuring nginx as a
local staging server. Updated deploy.sh to target either staging or prod
over SSH. Consolidated shared configuration between the two nginx nix
files and between the staging and prod deploy paths in deploy.sh.
2026-01-01 16:44:54 -08:00

151 lines
4.3 KiB
Nix

{ config, pkgs, lib, ... }:
let
# Read multiple API keys from the secrets file at build time
# Note: This embeds the secrets in the Nix store, which is a trade-off
# Alternative: Keep secrets file and read via njs module or external auth service
secretsFile = "/srv/nginx/secrets";
# Read API keys from file (one key per line, will be evaluated at build time)
# If the file doesn't exist yet, this will fail - create it first
apiKeysRaw = builtins.readFile secretsFile;
apiKeys = lib.filter (k: k != "") (lib.splitString "\n" apiKeysRaw);
# Generate map entries for each key
mapEntries = lib.concatMapStringsSep "\n "
(key: ''"Bearer ${key}" "authorized";'')
apiKeys;
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
appendHttpConfig = ''
# Check if the Authorization header matches any expected value
map $http_authorization $auth_status {
default "unauthorized";
"" "no_auth";
${mapEntries}
}
'';
# Virtual hosts configuration
virtualHosts = {
# Main website - Static HTML/CSS
"www.binning.net" = {
enableACME = true;
forceSSL = true;
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" = {
enableACME = true;
forceSSL = true;
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_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;
# 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;
'';
};
};
# Forgejo
"forgejo.binning.net" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:3000";
# No extraConfig needed - recommendedProxySettings handles headers
};
};
# Radicale
"radicale.binning.net" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:5232";
# recommendedProxySettings handles most headers
extraConfig = ''
proxy_set_header X-Script-Name "";
'';
};
};
};
};
# Firewall
networking.firewall.allowedTCPPorts = [ 80 443 ];
# ACME/Let's Encrypt
security.acme = {
acceptTerms = true;
defaults.email = "hvanb@pm.me";
};
}