{ config, lib, pkgs, ... }: let cfg = config.services.rsshub-oci; domain = config.networking.domain; subdomain = cfg.reverseProxy.subdomain; fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; images = { # https://github.com/DIYgod/RSSHub/pkgs/container/rsshub rsshub = pkgs.dockerTools.pullImage { imageName = "ghcr.io/diygod/rsshub"; imageDigest = "sha256:93660573e0fbfe1062e4fc512acf5043e1399519cdd9a11f130a8332306e8fdd"; hash = "sha256-cP2RnV6zmLoYzHgvuuHpqlRcNngD+YFRfRkFMNFQxG8="; finalImageName = "ghcr.io/diygod/rsshub"; finalImageTag = "2026-05-04"; }; # https://github.com/browserless/browserless/pkgs/container/chromium browserless = pkgs.dockerTools.pullImage { imageName = "ghcr.io/browserless/chromium"; imageDigest = "sha256:af3483eb7f125978d511df0d227d37931941b43d2cdb5f768da57263a7a132bf"; hash = "sha256-qKx/I9X/GTnoWpHY3gtZUoeL65ndOzU29bGjR6QLYp4="; finalImageName = "ghcr.io/browserless/chromium"; finalImageTag = "v2.48.2"; }; # https://github.com/hyoban/puppeteer-real-browser-hono/pkgs/container/puppeteer-real-browser-hono real-browser = pkgs.dockerTools.pullImage { imageName = "ghcr.io/hyoban/puppeteer-real-browser-hono"; imageDigest = "sha256:21bb4cd27144a61ca2f85eb3b4f555a074165bc6beaee318873ced0eb4046a04"; hash = "sha256-O8qV8Wfh75rhtEnSAlUBzBUVNGpypNF1sNY2F4zVqZE="; finalImageName = "ghcr.io/hyoban/puppeteer-real-browser-hono"; finalImageTag = "sha-6c6cbbc"; }; # https://hub.docker.com/_/redis redis = pkgs.dockerTools.pullImage { imageName = "redis"; imageDigest = "sha256:c5e375abb885e6b2021c0377879e4890bf76f9065b8922ffc113f2b226b9fc17"; hash = "sha256-ls1be+fp+chENJ7OrYng5EY3zdHsfiZCW3fmvwzwzj8="; finalImageName = "redis"; finalImageTag = "8.6.2-alpine"; }; }; defaultEnv = { NODE_ENV = "production"; CACHE_TYPE = "redis"; REDIS_URL = "redis://127.0.0.1:6379/"; PUPPETEER_WS_ENDPOINT = "ws://127.0.0.1:3000"; PUPPETEER_REAL_BROWSER_SERVICE = "http://127.0.0.1:3001"; }; inherit (lib) mkEnableOption mkIf mkOption mkOverride optional types ; inherit (lib.utils) mkReverseProxyOption mkVirtualHost ; in { options.services.rsshub-oci = { enable = mkEnableOption "RSSHub service stack with Podman"; port = mkOption { type = types.port; default = 1200; description = "The port RSSHub will listen on."; }; environment = mkOption { type = types.attrsOf types.str; default = { }; description = "Extra environment variables for RSSHub."; }; environmentFile = mkOption { type = types.nullOr types.path; default = null; description = "Environment file for secrets."; }; reverseProxy = mkReverseProxyOption "RSSHub" "rsshub"; }; config = mkIf cfg.enable { virtualisation.podman = { enable = true; autoPrune.enable = true; dockerCompat = true; }; services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { "${fqdn}" = mkVirtualHost { port = cfg.config.PORT; ssl = cfg.reverseProxy.forceSSL; }; }; networking.firewall.interfaces = let matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; in { "${matchAll}".allowedUDPPorts = [ 53 ]; }; virtualisation.oci-containers.backend = "podman"; virtualisation.oci-containers.containers = { rsshub-browserless = { image = with images.browserless; imageName + ":" + imageTag; imageFile = images.browserless; log-driver = "journald"; extraOptions = [ "--network=host" ]; }; rsshub-real-browser = { image = with images.real-browser; imageName + ":" + imageTag; imageFile = images.real-browser; log-driver = "journald"; environment = { PORT = "3001"; }; extraOptions = [ "--network=host" ]; }; rsshub-redis = { image = with images.redis; imageName + ":" + imageTag; imageFile = images.redis; volumes = [ "rsshub_redis-data:/data:rw" ]; log-driver = "journald"; extraOptions = [ "--network=host" ]; }; rsshub-rsshub = { image = with images.rsshub; imageName + ":" + imageTag; imageFile = images.rsshub; log-driver = "journald"; environment = defaultEnv // cfg.environment // { PORT = "${toString cfg.port}"; }; environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile; extraOptions = [ "--network=host" ]; }; }; systemd.services = let commonServiceCfg = { serviceConfig.Restart = mkOverride 90 "always"; partOf = [ "podman-compose-rsshub-root.target" ]; wantedBy = [ "podman-compose-rsshub-root.target" ]; }; in { podman-rsshub-browserless = commonServiceCfg; podman-rsshub-real-browser = commonServiceCfg; podman-rsshub-redis = commonServiceCfg // { after = [ "podman-volume-rsshub_redis-data.service" ]; requires = [ "podman-volume-rsshub_redis-data.service" ]; }; podman-rsshub-rsshub = commonServiceCfg // { after = [ "podman-rsshub-redis.service" "podman-rsshub-browserless.service" "podman-rsshub-real-browser.service" ]; }; podman-volume-rsshub_redis-data = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' podman volume inspect rsshub_redis-data || podman volume create rsshub_redis-data ''; partOf = [ "podman-compose-rsshub-root.target" ]; wantedBy = [ "podman-compose-rsshub-root.target" ]; }; }; systemd.targets."podman-compose-rsshub-root" = { unitConfig.Description = "Root target for RSSHub services."; wantedBy = [ "multi-user.target" ]; }; }; }