[{"content":"Desktop is CachyOS with Hyprland on an NVIDIA card, three monitors. Windows games run through Lutris, not Steam; Steam is only there for native Linux games like Dota 2.\nThe stack Lutris as the library, with umu as the launcher and proton-cachyos as the Proton build — the CachyOS package, so it updates with the system. ntsync is on out of the box (CachyOS kernel), so no esync/fsync fiddling. VRR only for fullscreen windows. The library is split into two Lutris categories, Flat and VR — the VR side has its own page. A workspace per game Every umu/Proton game gets the window class steam_app_default, real Steam games get steam_app_\u0026lt;id\u0026gt;. One Hyprland rule matching both makes every game fullscreen; a second one excludes launchers and installers so those stay windowed.\nOn top of that, a small Lua handler in my Hyprland config gives every game launch its own fresh workspace on the center monitor. When the game closes, the workspace disappears with it. Two games never land on top of each other, and nothing needs a per-game rule.\nNative Linux Steam games are the catch: they keep their own window class (Dota 2 is literally dota2). But Steam puts SteamAppId into the environment of everything it launches, so the handler also reads /proc/\u0026lt;pid\u0026gt;/environ and catches those too.\nInstalling is a right-click An install-game script does the whole thing in one go: unpack, create the Lutris entry, find the game on lutris.net for the cover art, add a launcher entry, add it to the streaming list. It sits in the file manager\u0026rsquo;s right-click menu on game archives. uninstall-game on a game folder removes all of it again and reports the freed space.\nOne thing I learned the hard way: Lutris rewrites its database when it exits. Any script that edits the database has to close Lutris first, or Lutris quietly restores the old rows.\nThe Battle.net corner WoW, Hearthstone and Battle.net share one prefix, and Battle.net is the fussiest part of the setup:\nTray icon. Wine apps use old-style XEmbed tray icons; my bar only speaks the newer StatusNotifier protocol. xembedsniproxy bridges the two. Exactly one instance may run. It never really exits. Closing Battle.net from the tray leaves its update agent running, which keeps the prefix alive — and the next game launched into that live session gets a fake GPU. A post-exit hook in Lutris now shuts the prefix down properly. It crashed once on every launch. An old --runonstartup argument from the Lutris install script swallowed the next argument. Deleting it fixed a crash dialog I\u0026rsquo;d been clicking away for weeks. WoW region. My game files are the EU build. \u0026ldquo;Locate game\u0026rdquo; refused them; \u0026ldquo;Install\u0026rdquo; pointed at the existing folder adopted them and only patched the difference. WoW beta. Launched by hand with PROTON_VERB=run — the default verb hangs while Battle.net holds the prefix. It also hung the GPU on entering the world, until I tried entering at the lowest graphics preset and raising it afterwards. Hearthstone sometimes loses permissions on its own game folder. A one-line chmod as Lutris pre-launch command fixes it before every start. Per-game fixes worth knowing Binding of Isaac saw my controller twice and scrambled the sticks. Disabling the duplicate DirectInput device in the prefix\u0026rsquo;s registry fixed it. Disabling dinput8 entirely does not — then the game won\u0026rsquo;t start. Dota 2 runs natively through Steam, with Minify for mods and cosmetics. Streaming to the living room The desktop runs Sunshine (started only when I need it); the living-room PC runs Moonlight.\nWhen a stream starts, a prep script turns off the two side monitors and switches the main one to 60 Hz. When it ends, the full layout comes back. The stream stays at native resolution and Sunshine scales it down for the TV. My main monitor can\u0026rsquo;t actually display a lower resolution, and games rendering at native look the same streamed or local. Every Lutris game is in Sunshine\u0026rsquo;s app list with its cover art. A small wrapper keeps Sunshine attached to the running game, so \u0026ldquo;quit app\u0026rdquo; in Moonlight really closes it. The living-room box A separate mini PC running Bazzite in Gaming Mode, driven entirely by controller:\nBoots straight into a library with just three tiles. Getting there took a Decky plugin for the tab order and a small script that navigates Steam\u0026rsquo;s UI through its debug port once it has loaded. YouTube is the real TV app (youtube.com/tv) in a kiosk Firefox with a smart-TV user agent and an ad blocker, mapped to the controller with a custom Steam Input layout. The Xbox controller wakes it from suspend. Night mode audio: a PipeWire filter chain with a compressor, so explosions don\u0026rsquo;t jump and dialogue stays audible at low volume. Left and right are swapped, because the soundbar sits behind the couch. ","permalink":"https://mariusladner.dev/setup/gaming/","summary":"How I play Windows games on Linux: Lutris and umu instead of Steam, a workspace per game, Battle.net, and streaming to the living room.","title":"Linux gaming"},{"content":"I got a Quest 3 and wanted PC VR on Linux without SteamVR. It works — better than I expected.\nThe stack WiVRn — streaming server on the PC, client app on the Quest. Works over WiFi or a USB cable; I use the cable. xrizer — for games that speak OpenVR (the SteamVR API), translates it to OpenXR. Games that speak OpenXR natively skip it and talk to WiVRn directly. Lutris + umu + Proton — same as my flat games. No Steam, no SteamVR, no ALVR. The chain is either game → Proton → xrizer → WiVRn → headset or, for OpenXR games, game → Proton → WiVRn → headset.\nWhat runs Game API Notes Half-Life: Alyx OpenVR → xrizer Needs the -vr launch argument. Texture memory one step below max, or it runs out of VRAM. Vertigo 2 OpenVR → xrizer The first game I got running through the Lutris setup. Gunman Contracts OpenVR → xrizer Just works. Wanderer: The Fragments of Fate OpenXR Stay on the custom graphics preset — the top preset crashes the GPU. Skydance\u0026rsquo;s BEHEMOTH OpenXR Needs the -steam launch argument, or it tries the Oculus entitlement check and quits. Ghost Town VR OpenXR Sound needs routing to the headset once (see below). SpaceEngine OpenVR → xrizer VR is a setting in its config, not a launch flag. The working directory must be the game root. Resident Evil 7 OpenXR (mod) Flat game + REFramework VR mod. Must run in DX11. Flat games made VR: Resident Evil 7 REFramework loads itself as dinput8.dll, which the game pulls in on its own — no injector needed. On Linux that only takes WINEDLLOVERRIDES=dinput8=n,b. Delete the mod\u0026rsquo;s openvr_api.dll and it uses OpenXR straight to WiVRn.\nSwitch the game to DX11. And turn the mod\u0026rsquo;s \u0026ldquo;force anti-aliasing\u0026rdquo; option off: RE Engine renders some effects at half resolution in a dither pattern and needs TAA to clean it up — without it, everything has a fine checkerboard over it.\nStandalone on the Quest The Simpsons: Hit \u0026amp; Run — a fan port that runs on the Quest itself. You copy your own game files onto the headset. The files must be vanilla: if yours come with a mod launcher pre-installed, strip it first.\nDesktop in the headset wayvr puts my Linux desktop into the headset as floating windows, and WiVRn starts it automatically. Handy for checking something without taking the headset off.\nWatching on the big screen Anyone in the living room can watch: Sunshine on the PC streams the desktop (with the game\u0026rsquo;s mirror window) to Moonlight on the living-room PC and the projector.\nThe catch is audio. Sunshine makes its own sink the default when a stream starts, games follow the default, and the headset goes silent. The fix is routing the game\u0026rsquo;s audio to both the headset and the stream — PipeWire\u0026rsquo;s WirePlumber remembers the choice per game.\nWhat doesn\u0026rsquo;t work (yet) HITMAN 3 — needs one OpenVR function (GetPosesForFrame) that xrizer, OpenComposite and VapoR all leave unimplemented. There\u0026rsquo;s an experimental xrizer branch; watch it. Hogwarts Legacy via UEVR — the mod injects, but no frames ever reach the headset. DX12 is the wall. The settings that matter Check which API a game uses before configuring it: openvr_api.dll in the game folder means OpenVR (→ xrizer); an openxr_loader.dll and no OpenVR means native OpenXR (→ no xrizer). Set WiVRn\u0026rsquo;s encoder to nvenc explicitly. On auto it picked a software encoder and everything was a black screen. gamescope: false for VR games in Lutris, or the game gets captured into a nested compositor and never reaches the headset. PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES=1 so the game can see WiVRn from inside Proton\u0026rsquo;s container. Don\u0026rsquo;t set XR_RUNTIME_JSON to a host path. Launch the real exe. Unreal games often have a tiny launcher shim in the root folder; point Lutris at ...-Win64-Shipping.exe instead. Open the firewall port (9757) for WiFi. The headset finds the PC via mDNS and then waits forever if the port is blocked. Over USB it doesn\u0026rsquo;t matter. No sound in the headset? The game\u0026rsquo;s audio went to the default output. Move it once to wivrn.sink (pactl move-sink-input) and WirePlumber remembers it. Watch VRAM. A big game plus WiVRn\u0026rsquo;s encoder plus the desktop fills the card fast; that shows up as texture glitches and judder, not as an error. Turn off automatic hand tracking on the Quest, or hands vanish whenever it switches from controllers to hands. On the list Blade \u0026amp; Sorcery, Red Matter 2, Into the Radius 2 and The Room VR are installed but untested.\n","permalink":"https://mariusladner.dev/setup/vr/","summary":"Quest 3 on Linux without SteamVR: WiVRn, xrizer and Proton. What runs, what doesn\u0026rsquo;t, the settings that matter.","title":"PC VR on Linux"},{"content":"The whole homelab is one small office mini PC running Ubuntu 24.04 and around 40 Docker containers. System and databases live on the internal SSD, media and bulk data on a big USB disk.\nRAM is the real limit — it\u0026rsquo;s all used, and swap is full of cold pages. So the rule is: nothing heavy. I looked at self-hosting GitLab once; it alone would need half the machine. Code stays on GitHub.\nHow it\u0026rsquo;s organised One Compose stack per app in /opt/docker/stacks, data in /opt/docker/appdata/\u0026lt;app\u0026gt;. Arcane is the web UI for all of it — deploys, logs, image updates. One trap: Arcane injects global variables at deploy time that live in its own database, not in any .env. Run docker compose up by hand and they silently resolve to empty strings. Traefik in front of everything, with a wildcard Let\u0026rsquo;s Encrypt certificate via Cloudflare DNS. Every app is appname.mariusladner.dev — full names, no abbreviations, no exceptions. AdGuard Home is the network\u0026rsquo;s DNS and DHCP server. It resolves *.mariusladner.dev to the server locally, and static leases get added through its API. Remote access is Twingate for everything, plus a Cloudflare Tunnel for the few things that need to work without it. Glance is the dashboard: one screen, no scrolling. Service health, media queue, what\u0026rsquo;s coming up, recently added. Beszel for server stats, ntfy for push notifications, Cronmaster for cron jobs with logs I can actually read. Maintenance that runs itself Daily updates at 6:05 — unattended apt upgrade. When the upgrade includes Docker itself, the script restarts every container that mounts the Docker socket, because they\u0026rsquo;d otherwise keep a dead socket and silently stop seeing changes. Daily: all Compose files and configs are pushed to a private GitHub repo. Weekly: appdata is mirrored to the USB disk. Image updates through Arcane. Lesson learned: when an app says \u0026ldquo;update available\u0026rdquo; but the updater says \u0026ldquo;no change\u0026rdquo;, the tag is usually dead — Immich froze its v2 tag when v3 came out, and Jellyseerr moved to a new project (Seerr) while the old image just stopped getting builds. What runs on it Media Jellyfin for movies and shows, with hardware transcoding and tone mapping on the iGPU. Seerr to request things, Sonarr and Radarr to manage them, Prowlarr for indexers, SABnzbd to download, Recyclarr for quality profiles. UmlautAdaptarr so German titles with umlauts actually get matched. Bazarr for subtitles — English and German, full and forced. Watched movies are cleaned up automatically a few days after playing (favourites stay). Radarr unmonitors a movie once its file is gone, so it doesn\u0026rsquo;t get downloaded all over again. Comics Komga as the reader, Komf for metadata, Kapowarr for new issues. One folder per series, named Series (year), files directly inside. Adding a comic is a Claude Code skill: \u0026ldquo;add the comic in my downloads\u0026rdquo; and it lands in the right folder with the right permissions.\nEverything else Immich — photos. Vaultwarden — passwords. Paperless-ngx — OCR\u0026rsquo;d documents, with mail intake from Gmail. Dawarich — my own location history instead of Google\u0026rsquo;s. Movary — movie diary. BentoPDF — PDF tools that don\u0026rsquo;t upload anything anywhere. Home Assistant Five dashboards in one shared layout: three columns, each a heading, the main control, its settings, its automations, one graph. My gecko\u0026rsquo;s terrarium: the light follows sunrise and sunset, a rain system mists every evening after lights-off, and a watchdog kills the pump if it ever runs longer than three minutes — whatever turned it on. Dog walks: a GPS tracker on the dog plus our phones answers \u0026ldquo;how long was he out, and with whom\u0026rdquo; — whoever\u0026rsquo;s phone is closest to the tracker walked him. A nightly AI recap sums up the day. Fitness data from my tracker via the Google Health integration. Smart plugs, lights and sensors around the flat. The WiFi problem The server is on WiFi. The router\u0026rsquo;s band steering kept bouncing it between 2.4 and 5 GHz — hundreds of roams in a few days, downloads at a quarter speed. It\u0026rsquo;s now pinned to prefer 5 GHz with a fallback to 2.4, in a hand-written wpa_supplicant config (netplan can\u0026rsquo;t express two priorities for one network), plus a watchdog that restarts the connection if it drops. Since the server is also the network\u0026rsquo;s DNS, when it drops, the whole flat loses DNS.\nClaude on the server The server also runs Claude Code in Remote Control mode as a service. From my phone I can start a session on the server and ask it to check or fix something while I\u0026rsquo;m out with the dog. It has a strict rulebook: no network changes, no reboots, never touch the gecko\u0026rsquo;s watchdog.\nWhat I\u0026rsquo;ve learned Verify behaviour, not config. Traefik ran a redirect rule for months that matched nothing, while its dashboard showed it as enabled. curl it. Idle doesn\u0026rsquo;t mean unused. FlareSolverr logs nothing and looks dead — but Kapowarr needs it, and its Compose stack won\u0026rsquo;t even start without it. Measure usage from DNS. Traefik has no access logs here, but AdGuard\u0026rsquo;s query log shows which apps actually get used — once you filter out the dashboard\u0026rsquo;s own health checks. ","permalink":"https://mariusladner.dev/setup/homelab/","summary":"One mini PC, around 40 containers: how it\u0026rsquo;s organised, what runs on it, and what keeps it alive.","title":"Homelab"},{"content":"I work in IT as a sysadmin, mostly with Ansible. At home I run a small homelab on a mini PC (Docker, Traefik, Home Assistant, Immich, Jellyfin and friends), a Hyprland desktop on CachyOS, and whatever else I\u0026rsquo;ve recently gotten into.\nThis site is where I write things down — especially the problems that took hours to figure out, and the ones that turned out to be impossible. If a post saves you an evening, it did its job.\n","permalink":"https://mariusladner.dev/about/","summary":"\u003cp\u003eI work in IT as a sysadmin, mostly with Ansible. At home I run a small homelab on a mini PC\n(Docker, Traefik, Home Assistant, Immich, Jellyfin and friends), a Hyprland desktop on CachyOS,\nand whatever else I\u0026rsquo;ve recently gotten into.\u003c/p\u003e\n\u003cp\u003eThis site is where I write things down — especially the problems\nthat took hours to figure out, and the ones that turned out to be impossible. If a post saves you\nan evening, it did its job.\u003c/p\u003e","title":"About"}]