144 lines
7.4 KiB
Nix
144 lines
7.4 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.
|
|
#
|
|
# 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.
|
|
#
|
|
# The web installer at /installer is unusable in v1.0.0-beta36: routes/base.php
|
|
# exempts that GET route from RedirectIfNotInstalled, but bootstrap/app.php
|
|
# appends the same middleware to the whole `web` group, which is what Livewire's
|
|
# update endpoint runs under. So every wizard click POSTs and gets 302'd back to
|
|
# step 1 while APP_INSTALLED=false. Installed from the CLI instead — migrate,
|
|
# then flip APP_INSTALLED=true in /var/lib/pelican-panel/.env. Recheck on the
|
|
# next image bump; if fixed, a fresh install can use the wizard again.
|
|
#
|
|
# FIRST-RUN (wings crash-loops until step 3 — that's expected):
|
|
# 1. Admin user: docker exec -it -u 82 pelican-panel php artisan p:user:make
|
|
# 2. Admin → Nodes → Create. FQDN games.nordhammer.it, SSL *on*, and
|
|
# "Behind Proxy" *on* — that last one is what makes the generated config
|
|
# say api.ssl.enabled: false, so wings serves plain HTTP and nginx owns
|
|
# the cert. The two daemon ports are not the same number: Listen stays
|
|
# 8080 (what wings binds inside the container) while Connect is 443 (what
|
|
# the panel and browsers dial, i.e. nginx).
|
|
# 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";
|
|
# Also required: once APP_INSTALLED=true the entrypoint blocks on
|
|
# `nc -z $DB_HOST $DB_PORT` for anything that isn't the literal string
|
|
# "sqlite". Unset means an endless "nc: bad port ''" boot loop, even
|
|
# though sqlite is already the app's own default.
|
|
DB_CONNECTION = "sqlite";
|
|
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"
|
|
# The panel calls the node over its public FQDN. Without this the
|
|
# container's DNS answers with the WAN address and the request
|
|
# hairpins out through the router and back into the same box — which
|
|
# works, but breaks whenever the WAN link flaps even though both ends
|
|
# are local. Pin it to the LAN IP; nginx serves a wildcard cert so
|
|
# SNI still matches.
|
|
"--add-host=games.nordhammer.it:10.0.0.1"
|
|
];
|
|
};
|
|
|
|
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/"
|
|
# Upstream's compose mounts the whole /etc/ssl/certs directory, which
|
|
# is useless on NixOS: every entry there is a symlink through
|
|
# /etc/static into /nix/store, so the container inherits dangling
|
|
# links and wings dies with "x509: certificate signed by unknown
|
|
# authority" when it calls the panel. Bind the single file instead —
|
|
# docker resolves the symlink host-side, so the container gets real
|
|
# PEM content. This is the path Go checks first on Linux.
|
|
"/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro"
|
|
];
|
|
ports = [
|
|
# Daemon API + console websocket — nginx fronts it as games.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;
|
|
};
|
|
};
|
|
}
|