# 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. # # AUTH: panel.nordhammer.it sits behind Authelia (see nginx.nix), except # /api/remote/ which wings needs. Pelican has no switch to disable its own # login, so its account is a second, redundant gate — replacing it means # standing up Authelia's OIDC provider and pointing Pelican's Socialite at it. # # 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, lib, ... }: { config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { systemd.tmpfiles.rules = [ # Panel runs as www-data (uid/gid 82, alpine) and writes its .env, # SQLite DB and storage into /pelican-data — root-owned host dirs make # the entrypoint die on "touch: /pelican-data/.env: Permission denied". # Numeric because no host user owns 82; nothing else on this box does. "d /var/lib/pelican-panel 0750 82 82 -" "d /var/lib/pelican-panel/logs 0750 82 82 -" # wings runs as root (distroless image, no USER directive). "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" ]; # 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"; # Required: with an https APP_URL the entrypoint refuses to start # unless either LE_EMAIL is set or it knows TLS is terminated # upstream. This also switches the bundled Caddy to plain :80 with # auto_https off, so no custom Caddyfile is needed. BEHIND_PROXY = "true"; APP_ENV = "production"; APP_DEBUG = "false"; # Without this Laravel sees the container's own IP and renders http:// # asset URLs. Caddy reuses the same value for trusted_proxies, so it # must be real CIDRs, not "*". nginx reaches us via the published # localhost port, so the request arrives from the docker bridge # gateway in 172.16/12 — not 127.0.0.1. TRUSTED_PROXIES = "172.16.0.0/12,10.0.0.0/8,127.0.0.1/32"; 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; }; }; }