{ 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 [ # Let's Encrypt cert for the panel domain — mounted read-only so # 3x-ui can serve the panel over its own TLS. webCertFile / # webKeyFile in the x-ui settings table must point at # /root/cert/fullchain.pem and /root/cert/key.pem respectively. "/var/lib/acme/${certDomain}/fullchain.pem:/root/cert/fullchain.pem:ro" "/var/lib/acme/${certDomain}/key.pem:/root/cert/key.pem:ro" ]; basePorts = [ "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" ]; realityPorts = # Only vds needs the 15380→443 forwarding that lets nginx stream # pass-through Xray REALITY while Xray itself sees the connection # arriving on 443 (matching its REALITY inbound config). 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:latest"; 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"; # Port-forwarded networking (replaces --network=host). # Common across all nodes that import this module: # 2049/tcp — 3x-ui web panel # 2096/tcp — subscription endpoint # 14380-15379/tcp+udp — Xray inbounds (matches firewall open range) # Vds-only (xlib.services.3x-ui.reality443Forwarding = true): # 15380→443/tcp — Xray REALITY inbound (nginx stream on 443 → 15380) # Adding a new inbound through the 3x-ui panel on a port outside # this range will require extending this list 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" ]; }; # Update "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 ''; }; # Builds # "podman-build-3xui_app" = { # path = [ # pkgs.podman # pkgs.git # ]; # serviceConfig = { # Type = "oneshot"; # TimeoutSec = 300; # }; # script = '' # cd /mnt/containers/3x-ui # podman build -t compose2nix/3xui_app -f ./Dockerfile . # ''; # }; }; # Root service # When started, this will automatically create all resources and start # the containers. When stopped, this will teardown all 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; }; }; # Folders tmpfiles.rules = [ "d ${xlib.dirs.services-mnt-folder} 0755 root root -" "d ${xlib.dirs.services-nodes-folder} 0755 root root -" "d ${xlib.dirs.services-nodes-folder}/${xlib.device.hostname} 0755 root root -" "d ${panel} 0755 root root -" "d ${panel}/db 0755 root root -" "d ${panel}/cert 0755 root root -" "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 ]; }; }; }