{ config, lib, pkgs, ... }: with lib; let cfg = config.services.meos-gateway; certDir = config.security.acme.certs."os.scoble.me".directory; in { options.services.meos-gateway = { enable = mkEnableOption "MeOS Gateway — nginx reverse proxy for os.scoble.me"; tailscaleIP = mkOption { type = types.str; description = "n97 Tailscale IP for nginx to bind to."; }; }; config = mkIf cfg.enable { # ── Homepage Dashboard config files ── # NOTE (scobleclaw#27): widgets.yaml is intentionally empty (no widgets). # The default "resources" widget hits /api/resources which 404s, producing # the 3 "API Error" chips at the top of the page. We replace the # operational surface with client-side per-service health probes injected # via /var/www/scobleclaw-assets/homepage-fixes.{css,js} (see the # scobleclaw-assets nginx location below). environment.etc."homepage/widgets.yaml".text = "[]"; environment.etc."homepage/settings.yaml".text = '' --- title: os.scoble.me theme: dark color: slate background: opacity: 50 showStats: false favicon: /icon.svg ''; environment.etc."homepage/bookmarks.yaml".text = '' --- - Speckl: - Wiki: - abbr: WK href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/ description: Business plans, whitepaper, strategy, leads - Landing Page: - abbr: LP href: https://speckl.scoble.me/ description: Software provenance standard site - Compiler Repo: - abbr: GH href: https://os.scoble.me/forgejo/sscoble/speckl description: Speckl spec and compiler source - Consulting: - abbr: CS href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Consulting description: $5K pilot offer and engagement model - Strategy: - abbr: ST href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Strategy description: Phased rollout and exit thesis - Meetup Kit: - abbr: MK href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Meetup-Reference-Card description: Event cheat sheet and follow-up templates - Lead Tracker: - abbr: LT href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Lead-Tracker description: Live tracking of prospect conversations - Whitepaper: - abbr: WP href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Dark-Provenance-Whitepaper description: Dark Provenance methodology paper - Social: - YouTube: - abbr: YT href: https://youtube.com/ - Sites: - Scott Scoble: - abbr: SS href: https://scott.scoble.me/ description: Speaker website - Speckl: - abbr: SP href: https://speckl.scoble.me/ description: Software provenance standard ''; environment.etc."homepage/services.yaml".text = '' --- # Layout restructured in scobleclaw#24 (cycle 128, agent-cos dispatch): # - "Infrastructure" split into "Operations" (observability) and "Tools" (end-user/operator tools) # - "Health" renamed "Family" (single tile — Family Health) # - "Status" merged into "Operations" (Uptime Kuma + Service Health + Grafana + Prometheus) # - Promoted Prometheus to a tile (k3s Service is healthy 1/1, ClusterIP 10.43.250.126:9090) # - Dropped Pool Manager tile (k3s pod ImagePullBackOff 5h+; service has no endpoints) # - Dropped Harbor tile (CrashLoopBackOff across core/db/exporter; half the stack down) # - Dropped Location tile (gps-share on :3993 returns 404 on every path; keep proxy block as # debug-only surface — removal requires nixos-rebuild which is currently blocked) # - "Spatial Office" in the proposed Tools list has no corresponding service/port on this host # (3987 = forgejo-mcp, 3988 = openbrain-mcp, both internal MCP servers); omitted pending # a real public URL. - Development: - Design System: href: https://os.scoble.me/system/ description: Scobleclaw design tokens, primitives, and patterns icon: mdi-palette-swatch - Forgejo: href: https://os.scoble.me/forgejo description: Self-hosted Git service icon: mdi-git - Presentations: - Podium: href: https://os.scoble.me/podium/ description: Presentation manager icon: mdi-presentation - Education: - WGU Speedrun: href: https://os.scoble.me/wgu/ description: Course tracker and journal icon: mdi-run-fast - Homeschool OS: href: https://os.scoble.me/homeschool/ description: Charlotte Mason curriculum management icon: mdi-school - Flashcards: href: https://os.scoble.me/flashcards/ description: SRS spaced repetition flashcard app icon: mdi-card-text-outline - Library: href: https://os.scoble.me/library/ description: Scobleclaw family library and catalog icon: mdi-bookshelf - Tutor: href: https://os.scoble.me/tutor/ description: AI tutor for homeschooling icon: mdi-robot-happy - Food: - Mealie: href: https://os.scoble.me/mealie/ description: Recipe management and meal planning icon: mdi-silverware - Family: - Family Health: href: https://os.scoble.me/health/ description: Family health tracking dashboard icon: mdi-heart-pulse - Operations: - Uptime Kuma: href: https://os.scoble.me/kuma/ description: Real-time uptime and incident monitoring icon: mdi-pulse - Service Health: href: https://os.scoble.me/health/deps/ description: Per-service health endpoint (probed every 60s) icon: mdi-heart-pulse - Grafana: href: https://os.scoble.me/grafana/ description: Infrastructure monitoring dashboards icon: mdi-chart-bar - Prometheus: href: https://os.scoble.me/prometheus/ description: Metrics, alerts, and PromQL (operators) icon: mdi-chart-line-variant - Tools: - Vault Drop: href: https://os.scoble.me/drop description: Upload files to vault icon: mdi-cloud-upload - Tickets: href: https://os.scoble.me/tickets/ description: Ticket status board icon: mdi-ticket - Style Prefs: href: https://os.scoble.me/style-prefs/ description: Shoppable style preference voting icon: mdi-tshirt-crew - Preview: href: https://os.scoble.me/preview/ description: Live preview and prototyping icon: mdi-eye - Umami: href: https://os.scoble.me/umami/ description: Web analytics icon: mdi-chart-line - Travel: - Trips: href: https://os.scoble.me/trips/ description: Trip planning and itinerary manager icon: mdi-airplane - External Sites: - Scott Scoble: href: https://scott.scoble.me/ description: Speaker & author website icon: mdi-web - Speckl: href: https://speckl.scoble.me/ description: Software provenance standard icon: mdi-shield-check ''; # ── Homepage Dashboard systemd service ── systemd.services.homepage-dashboard = { description = "Homepage Dashboard — landing page for os.scoble.me"; after = [ "network.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${pkgs.homepage-dashboard}/bin/homepage"; Restart = "on-failure"; RestartSec = "5"; DynamicUser = true; StateDirectory = "homepage"; WorkingDirectory = "/var/lib/homepage"; }; preStart = '' # Copy config from read-only /etc into writable /var/lib/homepage cp /etc/homepage/settings.yaml /var/lib/homepage/settings.yaml cp /etc/homepage/services.yaml /var/lib/homepage/services.yaml cp /etc/homepage/bookmarks.yaml /var/lib/homepage/bookmarks.yaml cp /etc/homepage/widgets.yaml /var/lib/homepage/widgets.yaml # Copy skeleton files from nix store so homepage can initialize for f in ${pkgs.homepage-dashboard}/share/homepage/src/skeleton/*.yaml; do base=$(basename "$f") if [ ! -f "/var/lib/homepage/$base" ]; then cp "$f" "/var/lib/homepage/$base" fi done mkdir -p /var/lib/homepage/logs ''; environment = { PORT = "8082"; HOMEPAGE_CONFIG_DIR = "/var/lib/homepage"; LOG_TARGETS = "stdout"; HOMEPAGE_ALLOWED_HOSTS = "os.scoble.me"; }; }; # ── Nginx — SSL-terminating reverse proxy with rate limiting ── services.nginx = { enable = true; additionalModules = [ pkgs.nginxModules.subsFilter ]; # Rate limit zones shared across the virtual host commonHttpConfig = '' # Login endpoint: max 5 req/s per IP, burst up to 10, delay excess limit_req_zone $binary_remote_addr zone=forgejo_login:10m rate=5r/s; # General API: max 30 req/s per IP limit_req_zone $binary_remote_addr zone=forgejo_api:10m rate=30r/s; limit_req_status 429; ''; virtualHosts."os.scoble.me" = { addSSL = true; sslCertificate = "${certDir}/fullchain.pem"; sslCertificateKey = "${certDir}/key.pem"; listen = [ { addr = cfg.tailscaleIP; port = 443; ssl = true; } ]; # ── Branded error pages (scobleclaw#36) ── # Catch framework-leaked 404/502/503/504 and serve Scobleclaw-branded # recovery pages from /var/www/scobleclaw/errors/. # Files are static HTML (no upstream), served via an internal location. # `proxy_intercept_errors on` ensures upstream 4xx/5xx (Next.js, app # frameworks) are also rewritten — otherwise we'd only catch errors # nginx generates itself. extraConfig = '' proxy_intercept_errors on; recursive_error_pages on; # Brand the status codes that framework apps leak by default error_page 404 /_scobleclaw/errors/404.html; error_page 502 503 504 /_scobleclaw/errors/5xx.html; # Internal: only nginx can fetch these (clients cannot request them # directly and we don't want them indexed by search engines). location ^~ /_scobleclaw/errors/ { internal; alias /var/www/scobleclaw/errors/; # Cache-bust on every request so a content edit shows up immediately, # but allow downstream gzip. Files are tiny (<8KB). add_header Cache-Control "public, max-age=300" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; gzip on; gzip_types text/html; gzip_min_length 256; expires 5m; } ''; locations = { "/forgejo/user/login" = { proxyPass = "http://127.0.0.1:3000/user/login"; extraConfig = '' # Rate limit login attempts aggressively limit_req zone=forgejo_login burst=10 nodelay; 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 X-Forwarded-Prefix /forgejo; ''; }; "/forgejo/user/sign_up" = { proxyPass = "http://127.0.0.1:3000/user/sign_up"; extraConfig = '' limit_req zone=forgejo_login burst=5 nodelay; 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 X-Forwarded-Prefix /forgejo; ''; }; "/forgejo/" = { proxyPass = "http://127.0.0.1:3000/"; proxyWebsockets = true; extraConfig = '' client_max_body_size 50M; 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 X-Forwarded-Prefix /forgejo; ''; }; "/preview/" = { proxyPass = "http://127.0.0.1:8085/"; proxyWebsockets = true; extraConfig = '' 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; sub_filter_once off; sub_filter_types text/html; sub_filter 'href="/' 'href="/preview/'; sub_filter 'src="/' 'src="/preview/'; sub_filter 'srcset="/' 'srcset="/preview/'; sub_filter ', /' ', /preview/'; sub_filter 'url("/' 'url("/preview/'; ''; }; "/podium/" = { proxyPass = "http://127.0.0.1:8090"; proxyWebsockets = true; extraConfig = '' 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 X-Forwarded-Prefix /podium; ''; }; "/drop/" = { proxyPass = "http://127.0.0.1:3002/"; proxyWebsockets = true; extraConfig = '' 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; ''; }; "/location/" = { proxyPass = "http://127.0.0.1:3993/"; extraConfig = '' 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; client_max_body_size 1M; ''; }; "/wgu/" = { proxyPass = "http://127.0.0.1:3005/"; proxyWebsockets = true; extraConfig = '' 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; ''; }; "/flashcards/" = { alias = "/var/www/flashcards.scoble.me/"; index = "index.html"; tryFiles = "$uri $uri/ /flashcards/index.html"; extraConfig = '' add_header Cache-Control "public, max-age=3600"; add_header X-Frame-Options "SAMEORIGIN" always; ''; }; "/library/" = { proxyPass = "http://127.0.0.1:8100/"; extraConfig = '' 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; ''; }; "/trips/" = { proxyPass = "http://127.0.0.1:8200/"; extraConfig = '' 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; ''; }; "/tickets/" = { proxyPass = "http://127.0.0.1:8099/"; extraConfig = '' 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 X-Forwarded-Prefix /tickets; proxy_hide_header Upgrade; proxy_http_version 1.1; ''; }; "/tickets/wsocket" = { proxyPass = "http://127.0.0.1:8096/"; proxyWebsockets = true; extraConfig = '' 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; ''; }; "/kuma/api/" = { proxyPass = "http://127.0.0.1:3001/api/"; extraConfig = '' 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; ''; }; "/kuma/" = { proxyPass = "http://127.0.0.1:3001/"; proxyWebsockets = true; extraConfig = '' 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_redirect / /kuma/; proxy_redirect /dashboard /kuma/dashboard; sub_filter_once off; sub_filter_types text/html; sub_filter '' ''; sub_filter 'href="/' 'href="/kuma/'; sub_filter 'src="/' 'src="/kuma/'; ''; }; "/grafana/" = { # scobleclaw#24 (cycle 128): was routing via traefik-ingress at 10.43.3.233:80 # which 404s in the new cluster. Direct to scobleclaw-monitoring/grafana Service. proxyPass = "http://10.43.150.72:3000"; proxyWebsockets = true; extraConfig = '' 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; ''; }; # scobleclaw#24 (cycle 128): new Prometheus tile. Direct to Service — same rationale # as Grafana above (avoid the traefik-ingress hop; the Service ClusterIP is reachable # from the gateway host because flannel.1 is in the 10.42.2.0/24 subnet of this node). # Subpath-friendly routing: use the same // convention as /kuma/. "/prometheus/" = { proxyPass = "http://10.43.250.126:9090/"; extraConfig = '' 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; # Prometheus emits absolute redirects from / to /graph etc. when behind a subpath. # Proxy_redirect rewrites the Location header back into our /prometheus/ prefix. proxy_redirect / /prometheus/; # sub_filter rewrites embedded hrefs in the HTML so links resolve through the gateway. sub_filter_once off; sub_filter_types text/html; sub_filter 'href="/' 'href="/prometheus/'; sub_filter 'src="/' 'src="/prometheus/'; ''; }; "/socket.io/" = { proxyPass = "http://127.0.0.1:3001/socket.io/"; proxyWebsockets = true; extraConfig = '' 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; ''; }; "/assets/" = { proxyPass = "http://127.0.0.1:3001/assets/"; extraConfig = '' add_header Cache-Control "public, max-age=31536000, immutable"; ''; }; "/health/deps/" = { proxyPass = "http://127.0.0.1:8101"; extraConfig = '' 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; ''; }; "/health/" = { alias = "/var/www/health.scoble.me/"; index = "index.html"; tryFiles = "$uri $uri/ /health/index.html"; extraConfig = '' add_header Cache-Control "public, max-age=3600"; add_header X-Frame-Options "SAMEORIGIN" always; ''; }; "/homeschool/" = { proxyPass = "http://127.0.0.1:3007"; proxyWebsockets = true; extraConfig = '' 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; ''; }; "/umami" = { proxyPass = "http://127.0.0.1:8069"; proxyWebsockets = true; extraConfig = '' 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; # Disable upstream gzip so sub_filter can match uncompressed content proxy_set_header Accept-Encoding ""; # Inject Scobleclaw design system into Umami HTML sub_filter_once off; sub_filter_types text/html; sub_filter '' ''; ''; }; "/umami/" = { proxyPass = "http://127.0.0.1:8069"; proxyWebsockets = true; extraConfig = '' 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; # Disable upstream gzip so sub_filter can match uncompressed content proxy_set_header Accept-Encoding ""; # Inject Scobleclaw design system into Umami HTML sub_filter_once off; sub_filter_types text/html; sub_filter '' ''; ''; }; "/style-prefs/" = { proxyPass = "http://10.43.40.183:8199/"; extraConfig = '' 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 X-Forwarded-Prefix /style-prefs; ''; }; "/" = { proxyPass = "http://127.0.0.1:8082"; proxyWebsockets = true; extraConfig = '' 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; # Disable upstream gzip so sub_filter can match uncompressed content proxy_set_header Accept-Encoding ""; # Inject Scobleclaw design system into the homepage HTML, # plus the per-ticket scobleclaw#27 fix overlay (CSS + JS). sub_filter_once off; sub_filter_types text/html; sub_filter '' ''; ''; }; # Scobleclaw design system explorer (canonical reference + CDN) # Serves the explorer at /system/ and acts as the CDN for tokens.css + primitives.js # under /system/assets/. Every scobleclaw app can import from this path. "/system/" = { alias = "/var/www/scobleclaw-design/"; extraConfig = '' add_header Cache-Control "public, max-age=300"; add_header X-Frame-Options "SAMEORIGIN" always; ''; }; "/system/assets/" = { alias = "/var/www/scobleclaw-design/assets/"; extraConfig = '' # Short max-age with revalidation. These assets are actively developed # (tokens.css, primitives.js), so we can't use `immutable` — that would # pin stale versions in every browser until manual cache clear. # version.json + ETag/Last-Modified drive revalidation; ?v=X.X.X in # index.html forces fresh fetches when we need an unblock. add_header Cache-Control "public, max-age=300, must-revalidate"; add_header X-Frame-Options "SAMEORIGIN" always; ''; }; # Static asset mount for the Scobleclaw design system (tokens.css + primitives.js) "/scobleclaw-assets/" = { alias = "/var/www/scobleclaw-assets/"; extraConfig = '' add_header Cache-Control "public, max-age=3600"; add_header X-Frame-Options "SAMEORIGIN" always; ''; }; # Homepage dashboard XHR endpoints — fetched client-side after initial render. # Must beat the catch-all /api/ below (which routes to Mealie for Nuxt JS). "/api/services" = { proxyPass = "http://127.0.0.1:8082/api/services"; extraConfig = '' 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; '';}; "/api/bookmarks" = { proxyPass = "http://127.0.0.1:8082/api/bookmarks"; extraConfig = '' 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; '';}; "/api/widgets" = { proxyPass = "http://127.0.0.1:8082/api/widgets"; extraConfig = '' 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; '';}; "/api/hash" = { proxyPass = "http://127.0.0.1:8082/api/hash"; extraConfig = '' 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; '';}; "/api/config/custom.css" = { proxyPass = "http://127.0.0.1:8082/api/config/custom.css"; extraConfig = '' add_header Cache-Control "public, max-age=300"; '';}; "/api/config/custom.js" = { proxyPass = "http://127.0.0.1:8082/api/config/custom.js"; extraConfig = '' add_header Cache-Control "public, max-age=300"; '';}; # Root-level /api/ needed for Nuxt JS XHR calls that don't include /mealie/ prefix "/api/" = { proxyPass = "http://10.43.253.234:9000/api/"; extraConfig = '' 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; ''; }; # Root-level /icons/ needed for Nuxt JS icon requests from manifest "/icons/" = { proxyPass = "http://10.43.253.234:9000/icons/"; extraConfig = '' 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; ''; }; "/mealie/" = { proxyPass = "http://10.43.253.234:9000/"; proxyWebsockets = true; extraConfig = '' 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 X-Forwarded-Prefix /mealie; # Disable upstream gzip so sub_filter can match uncompressed content proxy_set_header Accept-Encoding ""; sub_filter_once off; sub_filter_types text/html; sub_filter '' ''; sub_filter 'href="/' 'href="/mealie/'; sub_filter 'src="/' 'src="/mealie/'; sub_filter 'srcset="/' 'srcset="/mealie/'; sub_filter ', /' ', /mealie/'; sub_filter 'url("/' 'url("/mealie/'; sub_filter 'SUB_PATH:""' 'SUB_PATH:"/mealie"'; sub_filter 'baseURL:"/"' 'baseURL:"/mealie/"'; ''; }; # /tutor/ - host systemd unit (port 8300) is decommissioned and no # matching k8s Service exists yet. Until a ClusterIP is provisioned, # serve a maintenance page so the tile no longer 502s. # Closes engineering#68 (also addresses scobleclaw#23 partially). "/tutor/" = { alias = "/var/www/scobleclaw/maintenance/"; index = "tutor.html"; tryFiles = "$uri $uri/ /tutor/tutor.html"; extraConfig = '' add_header Cache-Control "no-cache, must-revalidate"; ''; }; }; }; # HTTP -> HTTPS redirect virtualHosts."os.scoble.me-http" = { listen = [ { addr = cfg.tailscaleIP; port = 80; } ]; serverName = "os.scoble.me"; extraConfig = "return 301 https://$server_name$request_uri;"; }; }; # ACME with Cloudflare DNS challenge security.acme = { acceptTerms = true; defaults = { email = "scott@scoble.me"; }; certs."os.scoble.me" = { dnsProvider = "cloudflare"; environmentFile = "/var/lib/acme/cloudflare.env"; group = "nginx"; }; }; # Firewall networking.firewall.allowedTCPPorts = [ 80 443 ]; }; }