# services/neko.nix — Guild Wars (2005) in a browser via Neko # # Streams an Xfce desktop running the Windows Guild Wars client (under Wine) # to a browser tab over WebRTC. Reach it at neko.nordhammer.it (Authelia-gated). # # Neko's stock images don't ship Wine, and apt installs land in /usr — which is # wiped whenever the container is recreated. So we bake Wine into a locally-built # image (FROM the upstream nvidia-xfce base) instead of relying on a volume. # Guild Wars' own data installs into the persistent /home/neko volume on first run. # # Rendering is software-only (no GPU): neko doesn't ship a prebuilt NVIDIA Xfce # desktop image, and building one from nvidia-base is a big detour for a 2005 # game. Wine renders via llvmpipe (software OpenGL) and neko encodes via x264 — # both are heavily multithreaded and this box has 56 Xeon threads to spare, so # Guild Wars is comfortably playable this way. { config, pkgs, lib, ... }: let # Wine-enabled image definition. Fed to `docker build` over stdin (see below) # so there's no build context — we have no COPY/ADD, and a Nix-store symlinked # context dir breaks BuildKit's Dockerfile resolution. Pinned to the v3.1 # series so the NEKO_MEMBER_*/NEKO_WEBRTC_* env schema below stays valid. dockerfile = pkgs.writeText "neko-gw.Dockerfile" '' FROM ghcr.io/m1k1o/neko/xfce:3.1 USER root RUN dpkg --add-architecture i386 \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ wine wine64 wine32 winbind ca-certificates wget \ && rm -rf /var/lib/apt/lists/* ''; in { config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { virtualisation.docker.enable = true; systemd.tmpfiles.rules = [ "d /var/lib/neko 0755 root root -" # The container's neko user is uid/gid 1000 and must own its home, or the # X server / Xfce can't create ~/.config, ~/.cache, etc. and the desktop # never starts. "d /var/lib/neko/home 0755 1000 1000 -" ]; systemd.services.neko = { description = "Neko — Guild Wars in a browser (Xfce + Wine + NVIDIA)"; after = [ "docker.service" "network-online.target" ]; requires = [ "docker.service" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; # First start pulls a multi-GB base image and runs apt — give it room. # If it fails, back off but don't crash-loop (see 7dtd veth-flood note). startLimitIntervalSec = 600; startLimitBurst = 3; serviceConfig = { Restart = "on-failure"; RestartSec = "30s"; TimeoutStartSec = "3600"; ExecStartPre = [ "-${pkgs.docker}/bin/docker rm -f neko" "${pkgs.writeShellScript "neko-build" '' exec ${pkgs.docker}/bin/docker build -t neko-gw:local - < ${dockerfile} ''}" ]; # Wrapped in a shell script so the ICE-server JSON survives quoting # (systemd's own ExecStart parser would strip the inner double quotes). ExecStart = pkgs.writeShellScript "neko-run" '' exec ${pkgs.docker}/bin/docker run --rm --name neko \ --shm-size=1g \ -p 127.0.0.1:8092:8080 \ -p 59000:59000/udp \ -e NEKO_DESKTOP_SCREEN=1280x720@30 \ -e NEKO_MEMBER_PROVIDER=multiuser \ -e NEKO_MEMBER_MULTIUSER_USER_PASSWORD=neko \ -e NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD=neko-admin \ -e NEKO_WEBRTC_UDPMUX=59000 \ -e NEKO_WEBRTC_NAT1TO1=10.0.0.1 \ -e 'NEKO_WEBRTC_ICESERVERS_FRONTEND=[{"urls":["stun:stun.l.google.com:19302"]}]' \ -e 'NEKO_WEBRTC_ICESERVERS_BACKEND=[{"urls":["stun:stun.l.google.com:19302"]}]' \ -v /var/lib/neko/home:/home/neko \ neko-gw:local ''; ExecStop = "${pkgs.docker}/bin/docker stop neko"; }; }; }; }