nixos/services/pelican.nix
rope 00ec9a9759 pelican: game server panel + wings containers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 11:57:15 +01:00

117 lines
4.7 KiB
Nix

# 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;
};
};
}