diff --git a/common.nix b/common.nix index a3ca361..4058d76 100644 --- a/common.nix +++ b/common.nix @@ -27,6 +27,7 @@ in # Services # ./services/server-permissions.nix ./services/game-servers.nix + ./services/pelican.nix # ./services/dr-server.nix ./services/qbittorrent-nox.nix ./services/nginx.nix diff --git a/ports.toml b/ports.toml index d6f2650..5e8c54a 100644 --- a/ports.toml +++ b/ports.toml @@ -25,6 +25,14 @@ name = "SSH" port = 22 protocol = "tcp" +# Pelican game-server allocation range (services/pelican.nix). wings publishes +# each game container's port on the host via the shared docker daemon, so these +# land on 10.0.0.1 directly. Keep in sync with the node's allocations in the panel. +[[forward]] +name = "Pelican game servers" +ports = "25565-25600" +protocol = "both" + # 7DTD forwards commented out — servers disabled in services/game-servers.nix. # [[forward]] # name = "7DTD game" diff --git a/services/nginx.nix b/services/nginx.nix index de643e8..74798dd 100644 --- a/services/nginx.nix +++ b/services/nginx.nix @@ -106,6 +106,22 @@ in "jellyfin.nordhammer.it" = proxy 8096; # streaming to external clients "seerr.nordhammer.it" = proxy 5055; # own auth via Jellyfin sign-in + # Pelican game panel — own auth. Not Authelia-protected: wings polls + # the panel API and the browser console opens a websocket straight to + # the node, neither of which can follow a forward-auth redirect. + "panel.nordhammer.it" = lib.recursiveUpdate (proxy 8092) { + locations."/".extraConfig = "client_max_body_size 100m;"; + }; + # wings daemon (see services/pelican.nix). Uploads through the file + # manager are whole server archives, so no body-size cap; the console + # websocket idles between keystrokes, hence the long read timeout. + "node.nordhammer.it" = lib.recursiveUpdate (proxy 8443) { + locations."/".extraConfig = '' + client_max_body_size 0; + proxy_read_timeout 7d; + ''; + }; + # --- Protected by Authelia --- "bazarr.nordhammer.it" = protectedProxy 6767; "sonarr.nordhammer.it" = protectedProxy 8989; diff --git a/services/pelican.nix b/services/pelican.nix new file mode 100644 index 0000000..ef465b9 --- /dev/null +++ b/services/pelican.nix @@ -0,0 +1,117 @@ +# services/pelican.nix — Pelican game-server panel (Pterodactyl's successor). +# +# Neither Pelican nor Pterodactyl is in nixpkgs, so both halves run as +# containers (like shelfarr/profilarr). Two pieces: +# +# panel — Laravel web UI. Single container, SQLite + file cache (no +# MariaDB/Redis needed). All state lives in /var/lib/pelican-panel. +# wings — Go daemon that actually starts game servers. Talks to the *host* +# docker daemon over docker.sock, so game containers are siblings, +# not nested. That's why every wings path below is mounted at the +# same path inside the container as outside: wings hands those paths +# to the host dockerd, which resolves them on the host. +# +# Neither is declarative — servers, eggs and users are configured in the web +# UI and stored in the panel's SQLite DB. Back up /var/lib/pelican-panel and +# /var/lib/pelican (server files); the nix side here is only the plumbing. +# +# FIRST-RUN (wings crash-loops until step 3 — that's expected): +# 1. https://panel.nordhammer.it → installer wizard, make admin user. +# 2. Admin → Nodes → Create. FQDN node.nordhammer.it, SSL *on*, port 443 +# (nginx terminates TLS and proxies to wings' plain :8080 below). +# Allocations: IP 10.0.0.1, ports 25565-25600 (matches ports.toml). +# 3. Node → Configuration tab → copy the generated YAML to +# /etc/pelican/config.yml on this host, then +# `systemctl restart docker-pelican-wings`. +{ config, pkgs, lib, ... }: +let + # Replaces the image's default Caddyfile, which would try to fetch its own + # Let's Encrypt cert. nginx already terminates TLS for us, so serve plain + # HTTP on :80 and trust the forwarded headers. private_ranges rather than a + # literal IP: requests arrive from the docker bridge gateway, not 127.0.0.1, + # and the published port is localhost-only anyway. + caddyfile = pkgs.writeText "pelican-Caddyfile" '' + { + admin off + servers { + trusted_proxies static private_ranges + } + } + + :80 { + root * /var/www/html/public + encode gzip + + php_fastcgi 127.0.0.1:9000 + file_server + } + ''; +in +{ + config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { + + systemd.tmpfiles.rules = [ + "d /var/lib/pelican-panel 0755 root root -" + "d /var/lib/pelican-panel/logs 0755 root root -" + "d /etc/pelican 0755 root root -" + "d /var/lib/pelican 0755 root root -" + "d /var/log/pelican 0755 root root -" + "d /tmp/pelican 0755 root root -" + ]; + + virtualisation.oci-containers.containers.pelican-panel = { + image = "ghcr.io/pelican-dev/panel:latest"; + volumes = [ + "/var/lib/pelican-panel:/pelican-data" + "/var/lib/pelican-panel/logs:/var/www/html/storage/logs" + "${caddyfile}:/etc/caddy/Caddyfile:ro" + ]; + # Localhost-only; nginx fronts it (see nginx.nix). + ports = [ "127.0.0.1:8092:80" ]; + environment = { + XDG_DATA_HOME = "/pelican-data"; + APP_URL = "https://panel.nordhammer.it"; + ADMIN_EMAIL = "fredrik@nordhammer.it"; + APP_ENV = "production"; + APP_DEBUG = "false"; + # Laravel otherwise renders http:// asset URLs behind the proxy. + TRUSTED_PROXIES = "*"; + TZ = "Europe/Stockholm"; + }; + extraOptions = [ "--add-host=host.docker.internal:host-gateway" ]; + }; + + virtualisation.oci-containers.containers.pelican-wings = { + image = "ghcr.io/pelican-dev/wings:latest"; + volumes = [ + "/var/run/docker.sock:/var/run/docker.sock" + "/var/lib/docker/containers/:/var/lib/docker/containers/" + "/etc/pelican/:/etc/pelican/" + "/var/lib/pelican/:/var/lib/pelican/" + "/var/log/pelican/:/var/log/pelican/" + "/tmp/pelican/:/tmp/pelican/" + "/etc/ssl/certs:/etc/ssl/certs:ro" + ]; + ports = [ + # Daemon API + console websocket — nginx fronts it as node.nordhammer.it. + "127.0.0.1:8443:8080" + # SFTP into server files. LAN-reachable; not in ports.toml, so no WAN. + "2022:2022" + ]; + environment.TZ = "Europe/Stockholm"; + # Image is distroless with no entrypoint script — needs a tty allocated. + extraOptions = [ "--tty" ]; + }; + + # Same guard the 7DTD containers had: wings exits immediately until + # /etc/pelican/config.yml exists, and an unbounded restart loop spawns a + # veth pair every few seconds, flooding systemd-networkd and risking WAN + # DHCP on this host (it's the router). Give up after 5 tries in 5 min. + systemd.services."docker-pelican-wings".serviceConfig = { + Restart = lib.mkForce "on-failure"; + RestartSec = "30s"; + StartLimitIntervalSec = 300; + StartLimitBurst = 5; + }; + }; +}