{ config, lib, pkgs, xlib, ... }: let panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/3x-ui"; certDomain = xlib.services."3x-ui".certDomain or null; certMounts = if certDomain == null then [ ] else # LE cert mounted read-only so 3x-ui can terminate TLS itself. # The 3x-ui settings table must point webCertFile / webKeyFile at # /root/cert/fullchain.pem and /root/cert/key.pem. map (f: "/var/lib/acme/${certDomain}/${f}:/root/cert/${f}:ro") [ "fullchain.pem" "key.pem" ]; basePorts = [ # 2049/tcp — 3x-ui web panel # 2096/tcp — subscription endpoint # 14380-15379/tcp+udp — Xray inbounds (matches firewall open range) "0.0.0.0:2049:2049/tcp" "0.0.0.0:2096:2096/tcp" "0.0.0.0:14380-15379:14380-15379/tcp" "0.0.0.0:14380-15379:14380-15379/udp" ]; # VDS-only: nginx stream forwards host:443 → host:15380 → container:443, # so Xray inside the container sees its REALITY inbound on its real # configured port 443. realityPorts = lib.optional xlib.services."3x-ui".reality443Forwarding "0.0.0.0:15380:443/tcp"; in { virtualisation = { podman = { enable = true; autoPrune = { enable = true; flags = [ "--all" ]; }; dockerCompat = true; }; oci-containers = { backend = "podman"; containers."3xui_app" = { image = "ghcr.io/mhsanaei/3x-ui:v3.7.0"; environment = { "XRAY_VMESS_AEAD_FORCED" = "false"; "XUI_ENABLE_FAIL2BAN" = "true"; "TZ" = "Europe/Moscow"; }; volumes = [ "${panel}/cert/:/root/cert:rw" "${panel}/db/:/etc/x-ui:rw" ] ++ certMounts; log-driver = "journald"; # Adding a new inbound through the 3x-ui panel on a port outside # the 14380-15379 range requires extending basePorts and rebuilding. ports = basePorts ++ realityPorts; }; }; }; systemd = { services = { "podman-3xui_app" = { serviceConfig.Restart = lib.mkOverride 90 "always"; partOf = [ "podman-compose-3x-ui-root.target" ]; wantedBy = [ "podman-compose-3x-ui-root.target" ]; }; "podman-update-3xui_app" = { path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; TimeoutSec = 300; }; script = '' podman pull ghcr.io/mhsanaei/3x-ui:latest systemctl restart podman-3xui_app.service ''; }; }; # Starts/stops together with all 3x-ui compose resources. targets."podman-compose-3x-ui-root" = { unitConfig.Description = "Root target generated by compose2nix."; wantedBy = [ "multi-user.target" ]; }; timers."podman-update-3xui_app" = { wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "weekly"; Persistent = true; }; }; tmpfiles.rules = [ (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") (xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root") (xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755" "root" "root" ) (xlib.helpers.mkTmpfile "d" panel "0755" "root" "root") (xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root") (xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root") # Relabel panel dir for SELinux so containers can access it. (xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root") ]; }; # Enable container name DNS for all Podman networks. networking.firewall = { allowedUDPPortRanges = [ { from = 14380; to = 15380; } ]; allowedTCPPortRanges = [ { from = 14380; to = 15380; } ]; interfaces = let matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; in { "${matchAll}".allowedUDPPorts = [ 53 ]; }; }; }