[{"content":"A small one: a bar button for Omarchy that opens a popup and plays a random video right there, inline, with a \u0026ldquo;Reroll\u0026rdquo; button for another pick — Random Video. Configure one or more sources once, then it\u0026rsquo;s a single click for something to have on in the background.\nWhat counts as a source Each configured source is one of two kinds:\nURL — something that already serves a different video on every request (a personal random-video endpoint, a redirect service), or just a plain direct video/YouTube link. Cmd — a shell command whose stdout is such a URL, for sites where the \u0026ldquo;random video\u0026rdquo; only exists behind real client-side JavaScript, not anything a plain fetch can scrape. That second case is the interesting one. ytroulette.com\u0026rsquo;s page has genuinely nothing to scrape — confirmed by checking, since even yt-dlp\u0026rsquo;s generic extractor fails on the raw page — because its own JS picks a random category and position client-side, POSTs those to its roulette.php, and gets back a video ID as JSON. resolvers/ytroulette.sh in the repo replicates that exact call and prints a plain youtube.com/watch?v=... URL — paste it in as a Cmd source, or use it as a template for another site that needs the same treatment.\nThe part that mattered more than the popup UI: YouTube\u0026rsquo;s split streams Modern YouTube\u0026rsquo;s default web client essentially never serves a single URL with both audio and video muxed together anymore — they come back as two separate streams by design, which breaks a plain inline \u0026lt;video\u0026gt;-style player that can only take one URL.\nThe fix was resolving YouTube links through yt-dlp\u0026rsquo;s android client (--extractor-args \u0026quot;youtube:player_client=android\u0026quot;) instead of the default one — noticeably faster (it skips slower client attempts that tend to fail anyway), and for most regular videos it still exposes the classic single muxed format the default client dropped, so the inline preview plays with sound most of the time.\nFor the videos where even that doesn\u0026rsquo;t yield a combined stream, there\u0026rsquo;s no way around it for an inline single-URL player — the preview plays silently, clearly labeled as such in the popup. \u0026ldquo;Open in window\u0026rdquo; always gets you guaranteed audio+video either way: when this plugin\u0026rsquo;s own resolution already has sound, mpv just plays that same URL directly; when it came back silent, mpv falls back to doing its own separate resolution via its ytdl hook.\nInstalling it omarchy plugin add https://github.com/AlanOne/omarchy-plugin-random-video --enable Requires yt-dlp (resolves every source to a real playable stream — if it\u0026rsquo;s missing or fails on a given URL, the plugin just falls back to treating that URL as already directly playable) and mpv for \u0026ldquo;Open in window.\u0026rdquo; Both are a normal part of an Arch/Omarchy setup already if you\u0026rsquo;ve done any video work on this machine.\nA note on Cmd sources A Cmd source runs exactly what you type, as your own user, via bash -c — no different from running the same command yourself in a terminal, but worth knowing plainly: it\u0026rsquo;s not sandboxed in any way.\nFull setup, the settings reference, and an optional uosc config for a nicer-looking \u0026ldquo;Open in window\u0026rdquo; player are all at github.com/AlanOne/omarchy-plugin-random-video.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/omarchy-random-video-plugin/","summary":"A bar popup that plays a random video inline with a Reroll button — and the YouTube split-stream problem that turned out to be the actual hard part.","title":"A 'Random Video' Button for the Omarchy Bar"},{"content":"Wanted my Windows PC\u0026rsquo;s physical keyboard and mouse to control this Omarchy (Hyprland) laptop sitting next to it — a same-desk software KVM, not remote desktop. Every mainstream tool for this (Synergy, Barrier, Input Leap, Deskflow) currently fails on Hyprland for the same underlying reason, so this turned into MWB Bridge: an Omarchy plugin with its own bundled Rust daemon that reimplements the wire protocol of Microsoft PowerToys\u0026rsquo; Mouse Without Borders instead.\nWhy the mainstream tools don\u0026rsquo;t work Synergy/Barrier/Input Leap/Deskflow all lean on the desktop portal\u0026rsquo;s RemoteDesktop interface to inject synthetic input on Linux — the sandboxed, permission-gated way a Wayland compositor is supposed to accept fake mouse/keyboard events from an app that isn\u0026rsquo;t the compositor itself. xdg-desktop-portal-hyprland doesn\u0026rsquo;t implement that portal yet, so every one of those tools fails the same way on Hyprland specifically, regardless of which one you pick.\nHyprland does expose the lower-level pieces directly, though: zwlr_virtual_pointer_v1 and zwp_virtual_keyboard_v1, the same wlroots protocols a compositor\u0026rsquo;s own tools use internally. Nothing stops a regular, unprivileged client from talking to those directly — there\u0026rsquo;s just no existing cross-platform KVM tool built to.\nWhy Mouse Without Borders specifically Rather than write a new protocol from scratch (and a new Windows-side client to go with it), MWB Bridge\u0026rsquo;s daemon speaks the real wire protocol of PowerToys\u0026rsquo; Mouse Without Borders — genuinely MIT-licensed, so the actual C# source was available to check byte-for-byte, not just guessed at from watching traffic. The result: a normal, unmodified PowerToys install on Windows, pointed at this machine\u0026rsquo;s IP and a shared key, works with zero changes on the Windows side. All the new code lives in this daemon.\nThe wire protocol turned out to be the easy part, confirmed correct early on against real PowerToys debug dumps. The actual blockers were all Windows-side state the classic Mouse Without Borders settings UI doesn\u0026rsquo;t reliably persist — matrix/machine-pool entries that silently don\u0026rsquo;t save, hostname-resolution races, that kind of thing. The full writeup (byte offsets, handshake sequence, every one of those Windows-side gotchas) is in the repo\u0026rsquo;s PROTOCOL.md for anyone hitting the same wall.\nWhat actually works Mouse movement, clicks (including a Logitech-style back/forward side button), and vertical/horizontal scroll. Keyboard, correctly mapped for a Slovenian (QWERTZ) layout — see below for the catch on other layouts. Switching machines either by moving the cursor off the shared screen edge, or PowerToys\u0026rsquo; own Ctrl+Alt+F1-style hotkey. Clipboard sync, both directions: plain text and images, small or large (a full-resolution screenshot goes over the same path as a file transfer, automatically). Copying a file on Windows makes it pasteable here as a real file — the reverse direction doesn\u0026rsquo;t work yet (see Known bugs). Double-tapping Mouse Without Borders\u0026rsquo; own lock hotkey on Windows locks this machine too, within about half a second. An optional patch that dismisses Omarchy\u0026rsquo;s decorative screensaver on raw cursor movement, not just a keypress — useful specifically because control usually arrives here as a mouse move with no keyboard involved. Runs as an auto-starting, auto-reconnecting systemd --user service, including reconnecting immediately on laptop wake instead of waiting out a socket timeout (a subscription to logind\u0026rsquo;s own suspend/resume signal — the same trick Omarchy\u0026rsquo;s own pre-suspend lock screen uses). Known bugs Keyboard layout mapping is only verified against Slovenian. Windows reassigns punctuation/OEM key codes per active layout in ways that aren\u0026rsquo;t derivable from the key code alone, so another layout will likely need its own empirical fixes — PROTOCOL.md documents exactly how the Slovenian ones were worked out, as a template. Occasional duplicate keystrokes, not yet root-caused — a live wire-traffic capture during testing showed a clean 1:1 key-down/key-up pair every time, so it\u0026rsquo;s rarer than easy to reproduce on demand. File transfer back to Windows (this machine → Windows) doesn\u0026rsquo;t work, despite a genuinely thorough investigation — the exact real machine-switch packet sequence gets sent, byte-for-byte matching a live capture from a real PowerToys switch, and Windows still never connects to pull it. Documented as an investigated, unresolved limitation, not abandoned after a quick guess. Installing it omarchy plugin add https://github.com/AlanOne/omarchy-plugin-mwb-bridge.git --enable Then set the shared security key (from PowerToys\u0026rsquo; Mouse Without Borders settings), this Windows PC\u0026rsquo;s address, and a name for this machine from the bar widget\u0026rsquo;s popup — the plugin builds and installs the daemon itself on first load, nothing to compile by hand.\nFull setup steps, the complete protocol writeup, and the current bug list are all at github.com/AlanOne/omarchy-plugin-mwb-bridge.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/omarchy-mwb-bridge-software-kvm/","summary":"Why Synergy, Barrier, Input Leap, and Deskflow all fail on Hyprland, and the Rust daemon I built instead — reimplementing PowerToys\u0026rsquo; Mouse Without Borders wire protocol against Hyprland\u0026rsquo;s own wlroots input protocols.","title":"A Software KVM for Hyprland, Since Synergy/Barrier Won't Work"},{"content":"Wanted a quick way to check the cameras without opening an app, so this turned into an actual Omarchy plugin: Cameras, a bar-widget with a snapshot pill, a popup camera switcher, and a one-click live-view launch via mpv. It works with Reolink cameras and Google Nest cameras, and — the part that took the most iteration — it doesn\u0026rsquo;t hardcode either.\nWhy it doesn\u0026rsquo;t talk to the cameras directly The plugin itself is deliberately thin. All the actual camera protocol handling — RTSP, ONVIF, and Nest\u0026rsquo;s OAuth-plus-WebRTC mess — is done by go2rtc, a small self-hosted streaming server. The plugin only ever talks to go2rtc\u0026rsquo;s local HTTP API: list streams, add a stream, delete a stream, grab a JPEG snapshot. See the self-hosted apps roundup if you haven\u0026rsquo;t run something like this before — go2rtc is a docker compose up -d away, same as everything else on that list.\nPlugins in Omarchy\u0026rsquo;s shell run unsandboxed, in the same long-running process as the rest of the bar. That\u0026rsquo;s exactly the kind of place you don\u0026rsquo;t want a hand-rolled RTSP/WebRTC client living. go2rtc already does it correctly; the plugin\u0026rsquo;s job is UI and a handful of HTTP calls.\nThe part that mattered more than the streaming: no hardcoded cameras First pass had the camera\u0026rsquo;s stream name baked into the plugin\u0026rsquo;s settings. Fine for one camera, useless for a marketplace listing — nobody installing this plugin has a camera called e330_sub sitting in their config already.\nFix ended up being straightforward once go2rtc\u0026rsquo;s own API contract clicked: PUT /api/streams?name=X\u0026amp;src=Y both starts a stream live and writes it into go2rtc\u0026rsquo;s config file for you. So the popup just has an \u0026ldquo;Add camera\u0026rdquo; form — a name, a stream URL, optionally a lower-res URL for the thumbnail — and it calls that endpoint directly. DELETE works the same way in reverse. A fresh install starts with zero cameras and the popup opens straight to that form. Multiple cameras get grouped automatically and shown as tabs across the top of the popup.\nSetting it up: Reolink Any Reolink camera with direct RTSP/ONVIF support works — flip on RTSP in the Reolink app (Device Settings → Advanced Network Settings → Server Settings), then it\u0026rsquo;s a stream URL: rtsp://user:pass@camera-ip:554/Preview_01_main. Paste that into the popup\u0026rsquo;s Add Camera form and it shows up immediately.\nThe E330 is what this was built against — no hub or NVR required, straight RTSP off the camera. If you\u0026rsquo;re shopping for one specifically because you want this kind of direct-access setup, check the spec sheet for RTSP/ONVIF support before buying; some of Reolink\u0026rsquo;s battery/wifi-only lineup only works through a Reolink Home Hub or NVR, which is a perfectly fine setup too, just point the stream URL at the hub\u0026rsquo;s RTSP output instead of the camera\u0026rsquo;s.\nSetting it up: Google Nest This is the one that ate an afternoon. Nest cameras go through Google\u0026rsquo;s Smart Device Management API, which means: a Google Cloud project, a one-time $5 Device Access registration, an OAuth consent flow, and a device listing call before you even have a device ID to work with. go2rtc\u0026rsquo;s nest:// source handles the actual WebRTC negotiation once it has credentials — getting the credentials is the whole exercise.\nFull step-by-step (Cloud project, OAuth client, Device Access registration, the authorization URL, exchanging the code for a refresh token, listing devices) is in the plugin\u0026rsquo;s README. One thing worth flagging in advance: if a Nest Doorbell shows up as \u0026ldquo;unreachable,\u0026rdquo; check the battery before assuming the setup is wrong — it needs to be awake to answer a stream request, and that\u0026rsquo;s the most likely reason it isn\u0026rsquo;t.\n(Affiliate links — see the about page for the disclosure.)\nGrabbing the plugin omarchy plugin add https://github.com/AlanOne/omarchy-plugin-cameras.git --enable Repo, full setup guides for both camera types, and the security notes on what the plugin actually talks to are all at github.com/AlanOne/omarchy-plugin-cameras.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/omarchy-camera-widget-go2rtc/","summary":"A snapshot pill in the Omarchy bar that switches between however many cameras you\u0026rsquo;ve got, with a popup UI to add or remove them — no config file editing, no fixed camera count.","title":"Building a Camera Bar Widget for Omarchy (Reolink + Google Nest)"},{"content":"Every API tutorial covers versioning in a paragraph. Every real API that\u0026rsquo;s survived past its first year has a versioning story that\u0026rsquo;s messier than the tutorial suggested, because the actual hard part isn\u0026rsquo;t picking a scheme — it\u0026rsquo;s living with it once real clients depend on it.\nThe three real options URL versioning (/v1/users, /v2/users) — the most common approach, and for good reason: it\u0026rsquo;s visible, cacheable, and trivially easy for a client to understand and route on. The honest downside is that it makes \u0026ldquo;the resource\u0026rdquo; and \u0026ldquo;the version of the resource\u0026rdquo; look like they\u0026rsquo;re the same thing in the URL, which they conceptually aren\u0026rsquo;t — /v1/users/42 and /v2/users/42 are the same user, but nothing about the URL structure communicates that.\nHeader versioning (Accept: application/vnd.yourapi.v2+json, or a custom X-API-Version header) — keeps the URL clean and correctly signals that versioning is a representation concern, not a resource-identity concern. The real cost: it\u0026rsquo;s invisible in browser address bars, harder to test with a quick curl command from memory, and easy for API consumers to simply forget to set — meaning your \u0026ldquo;default\u0026rdquo; version behavior needs to be deliberately chosen, not accidental.\nNo versioning, only additive changes — the strategy of never introducing a breaking change at all: only ever add new optional fields, never remove or rename existing ones, never change a field\u0026rsquo;s meaning. This is the strategy that actually requires the most discipline of the three, but it\u0026rsquo;s the only one that avoids the \u0026ldquo;how long do we support v1\u0026rdquo; problem entirely, because there\u0026rsquo;s only ever one version. Stripe is the most commonly cited real-world example of running this model successfully at scale for years.\nThe part nobody puts in the tutorial: deprecation Picking a versioning scheme is the easy 10%. The actual hard part is what happens 18 months later when you want to retire v1 and half your integrations still use it. A few things that actually work in practice:\nPut a deprecation date in the response itself, not just documentation nobody reads — a Deprecation or Sunset HTTP header (both are real, standardized headers for exactly this) that API monitoring tools and thoughtful client developers will actually notice. Log which version each request uses, from day one, before you have any reason to care yet. When you eventually want to deprecate v1, \u0026ldquo;nobody\u0026rsquo;s called that in 90 days\u0026rdquo; is a decision you can only make if you were already logging it. Never set a hard cutoff date until usage is genuinely near zero. A grace period that gets extended once because \u0026ldquo;one enterprise client needs three more months\u0026rdquo; is normal and fine. A grace period you refuse to extend because you already announced it publicly is how you lose that client\u0026rsquo;s trust in every future announcement you make. The honest recommendation For a new API with no existing constraints: prefer additive-only changes for as long as you possibly can, and reach for URL versioning as the fallback the first time you genuinely need a breaking change (renaming a field, changing a response shape, changing required-vs-optional). It\u0026rsquo;s the combination that\u0026rsquo;s easiest for API consumers to reason about, and the one most real production APIs converge on even when they started somewhere else.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/api-versioning-strategies/","summary":"Every API eventually needs to change in a way that breaks someone. The question isn\u0026rsquo;t whether to version — it\u0026rsquo;s which strategy you can actually maintain two years in.","title":"API Versioning Strategies That Actually Work in Production"},{"content":"The pitch for Linux gaming used to be \u0026ldquo;it mostly works if you\u0026rsquo;re patient.\u0026rdquo; That\u0026rsquo;s no longer the honest framing — thanks largely to Valve\u0026rsquo;s investment in Proton (driven by the Steam Deck), most single-player and many multiplayer games on Steam run at or near native performance. The remaining friction is specific and mostly predictable, not random.\nCheck ProtonDB before you buy, not after ProtonDB aggregates real user reports per game, rated Platinum (runs perfectly out of the box) down to Borked (doesn\u0026rsquo;t run). This is the single most useful habit to build if you\u0026rsquo;re gaming on Linux — check the rating before purchasing, not after you\u0026rsquo;ve already bought something that turns out to need workarounds. Steam Deck verification badges (also visible directly in the Steam store) are a decent proxy for the same information, since Deck verification specifically tests Proton compatibility.\nThe actual failure mode: anti-cheat The single biggest predictable category of \u0026ldquo;this won\u0026rsquo;t work\u0026rdquo; is kernel-level anti-cheat. Games using Easy Anti-Cheat or BattlEye can work under Proton — many publishers have explicitly enabled Linux support for their anti-cheat integration — but it\u0026rsquo;s opt-in per game, not automatic. A game using the same anti-cheat middleware as another game that works fine on Linux is not a guarantee; each publisher has to flip that switch. Check Are We Anti-Cheat Yet for current per-game status before assuming a competitive multiplayer title will work.\nSetting it up: it\u0026rsquo;s simpler than it sounds On Steam specifically, enabling Proton for all titles is one setting: Steam → Settings → Compatibility → \u0026ldquo;Enable Steam Play for all other titles,\u0026rdquo; then pick a Proton version (Proton Experimental is usually the right default unless a specific game has a known-good older version documented on ProtonDB). For games outside Steam entirely — Epic, GOG, standalone installers — Lutris and Heroic Games Launcher handle Proton/Wine integration for those storefronts specifically, with community-maintained install scripts for common problem games.\nThe tools worth knowing about ProtonDB — compatibility ratings, described above Lutris — a launcher that manages Wine/Proton prefixes for non-Steam games, with community install scripts that handle the fiddly per-game workarounds automatically Heroic Games Launcher — the equivalent for Epic Games Store and GOG specifically, cleaner and more focused than Lutris if that\u0026rsquo;s all you need MangoHud — an in-game performance overlay (FPS, frame time, temps) that works across Proton titles, useful for actually diagnosing whether a performance issue is Proton overhead or something else GameMode — a Feral Interactive tool that requests performance-governor CPU behavior and other optimizations automatically while a game is running, then reverts after What to expect realistically Single-player games with no kernel-level anti-cheat: high confidence it\u0026rsquo;ll just work, usually within a few percent of native Windows performance. Competitive multiplayer titles: check Are We Anti-Cheat Yet first, every time, since this is the one category where \u0026ldquo;probably fine\u0026rdquo; isn\u0026rsquo;t good enough odds. Anything requiring specialized peripherals with Windows-only drivers (some racing wheels, some VR headsets) is the other predictable trouble spot — check for Linux-specific driver support before assuming Proton alone solves it.\nSteam Deck\u0026rsquo;s existence has been the accelerant here — every game a big publisher wants Deck-verified now gets real Linux compatibility testing, which benefits desktop Linux gaming as a side effect. That trend is the actual reason this is worth taking seriously now, not a philosophical argument about open platforms.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/linux-gaming-proton-guide/","summary":"Proton has made Linux gaming genuinely viable, not just theoretically possible. Here\u0026rsquo;s the honest state of it: what to check before buying a game, and the tools that actually matter.","title":"Gaming on Linux in 2026: What Proton Actually Handles and What Still Needs Work"},{"content":"Laravel and Craft CMS solve different problems — one is an application framework, the other is a content-first CMS with a framework underneath (Yii2, not Laravel\u0026rsquo;s stack) — but enough Laravel developers end up doing Craft work that it\u0026rsquo;s worth being specific about which instincts carry over cleanly and which will actively slow you down.\nWhat transfers cleanly Eloquent-style thinking, mostly. Craft\u0026rsquo;s ElementQuery API (Entry::find()-\u0026gt;section('blog')-\u0026gt;all()) reads close enough to Eloquent\u0026rsquo;s query builder that the mental model transfers almost immediately — fluent, chainable, lazy until you call a terminal method like .all() or .one().\nService-container instincts. Craft is built on Yii2, which has its own dependency injection container and service-location patterns (Craft::$app-\u0026gt;getEntries(), Craft::$app-\u0026gt;getElements()). It\u0026rsquo;s not Laravel\u0026rsquo;s container API, but the underlying idea — resolve services through a central application object rather than instantiating everything directly — is the same shape.\nMigrations. Craft has real migration classes (craft\\db\\Migration, safeUp()/safeDown()) that will feel immediately familiar if you\u0026rsquo;ve written Laravel migrations. The plugin installation lifecycle even auto-runs a plugin\u0026rsquo;s Install migration by convention, similar in spirit to how Laravel discovers and runs migrations.\nWhat doesn\u0026rsquo;t transfer, and will cost you time if you assume it does There\u0026rsquo;s no Eloquent model per content type. In Laravel, a Post model maps to a posts table you control directly. In Craft, all content lives in the generic elements table structure regardless of type — Entries, Assets, Users, Categories all share that backbone, with type-specific data attached separately. You don\u0026rsquo;t get to define your own schema for a \u0026ldquo;Product\u0026rdquo; the way you\u0026rsquo;d define a Laravel model\u0026rsquo;s table; you configure a Section and Entry Type through Craft\u0026rsquo;s content modeling system instead.\nNo Artisan-equivalent scaffolding for content generation. This was enough of a gap that it\u0026rsquo;s worth its own callout: Laravel\u0026rsquo;s php artisan make:factory / db:seed workflow for generating realistic demo content has no first-party Craft equivalent. If you\u0026rsquo;re doing Craft client work and miss that workflow specifically, that\u0026rsquo;s a real, common pain point — Sprout brings the same factory()-\u0026gt;count()-\u0026gt;create() pattern to Craft console commands, which is exactly the gap this section is describing.\nProject config, not .env + migrations, is the source of truth for structure. Craft\u0026rsquo;s project config system tracks content structure (sections, fields, field layouts) as YAML files meant to be committed to version control and applied across environments — it\u0026rsquo;s a different mental model from Laravel\u0026rsquo;s \u0026ldquo;migrations define schema, .env defines environment-specific values\u0026rdquo; split. Fighting this instead of learning it will cause real merge-conflict pain on a team.\nTwig, not Blade. Obvious on the surface, easy to underestimate in practice — Twig\u0026rsquo;s whitespace control, filter syntax, and template inheritance model are different enough that muscle memory from Blade will actively mislead you on the syntax details, not just the tag names.\nThe honest takeaway A Laravel background makes you a faster Craft developer than someone with no PHP framework experience at all, but not because the frameworks are similar — it\u0026rsquo;s because you already have the right instincts about where to look for the equivalent concept, even when the concept itself works differently underneath. Budget real time to read Craft\u0026rsquo;s own docs rather than assuming Laravel experience alone gets you there.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/laravel-to-craft-cms/","summary":"Both are PHP, both are opinionated, and that\u0026rsquo;s about where the similarity stops being useful. Here\u0026rsquo;s what a Laravel developer\u0026rsquo;s instincts get right and wrong on a first Craft CMS project.","title":"Laravel to Craft CMS: What Actually Transfers, and What Doesn't"},{"content":"The self-hosted apps guide on this site already flagged Caddy as worth it \u0026ldquo;for how little configuration it needs compared to nginx for the same result.\u0026rdquo; Here\u0026rsquo;s exactly how little.\nThe whole config, for one app This is a complete, working Caddyfile — not a trimmed-down example, the actual entire file:\nyourdomain.com { reverse_proxy localhost:3000 } That\u0026rsquo;s it. Point a domain\u0026rsquo;s DNS at your server, run Caddy with this file, and you get your app served over HTTPS with a real, auto-renewing certificate — Caddy provisions it from Let\u0026rsquo;s Encrypt automatically the first time it sees a request for that domain. No separate certbot setup, no cron job to renew certificates, no nginx config with a server block for port 80 that redirects to another server block for port 443.\nMultiple apps, still one file This is where a reverse proxy stops being a nice-to-have. Once you\u0026rsquo;re running more than one self-hosted service, a reverse proxy is what lets them all live behind port 443 on a single server, routed by domain:\napp-one.yourdomain.com { reverse_proxy localhost:3000 } app-two.yourdomain.com { reverse_proxy localhost:8080 } home.yourdomain.com { reverse_proxy localhost:8096 } Each block is independent. Add a new self-hosted app, add four lines, reload Caddy — no juggling port numbers in your head when you\u0026rsquo;re trying to remember which service was on which port.\nVerified with Docker The Docker Compose pattern is just as short, and this exact setup was tested end to end — Caddy reverse-proxying to a backend container by service name, real HTTP response coming back through Caddy on the frontend port:\nservices: caddy: image: caddy:2 ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; volumes: - ./Caddyfile:/etc/caddy/Caddyfile your-app: image: your-app-image yourdomain.com { reverse_proxy your-app:80 } Docker Compose\u0026rsquo;s built-in service-name DNS resolution means your-app in the Caddyfile just works — no need to look up container IPs.\nOne honest caveat The reverse-proxy mechanics above are tested and confirmed working. The automatic HTTPS certificate issuance specifically requires a real public domain with DNS pointed at a publicly reachable server on ports 80/443 — that part wasn\u0026rsquo;t (and can\u0026rsquo;t be) verified from a local sandbox, though it\u0026rsquo;s Caddy\u0026rsquo;s most well-documented and widely-relied-on feature.\nIf you don\u0026rsquo;t have a server to point a domain at yet, that\u0026rsquo;s the one prerequisite this whole setup depends on — a small DigitalOcean droplet is enough for everything described here, covered in more detail in the old laptop to home server post if you\u0026rsquo;d rather use hardware you already own instead.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/caddy-reverse-proxy-https/","summary":"Once you\u0026rsquo;re running more than one self-hosted app, a reverse proxy stops being optional. Caddy gets you there with a config file short enough to read in ten seconds.","title":"Reverse Proxy with Caddy: The Only Config You Need for HTTPS"},{"content":"Most systemd tutorials hand you a unit file to copy-paste without explaining what any of it does. That\u0026rsquo;s fine until you need to write your own for a script that doesn\u0026rsquo;t come with one — which is most of the time if you\u0026rsquo;re self-hosting anything custom.\nThe three sections you actually need A minimal working unit file has three blocks. Here\u0026rsquo;s one that keeps an arbitrary script running and restarts it if it crashes:\n[Unit] Description=Heartbeat test service After=network.target [Service] Type=simple ExecStart=/home/you/scripts/heartbeat.sh Restart=on-failure RestartSec=3 [Install] WantedBy=default.target [Unit] — metadata and ordering. After=network.target just means \u0026ldquo;don\u0026rsquo;t start this before the network is up,\u0026rdquo; not a hard dependency — use Requires= alongside After= if the service genuinely can\u0026rsquo;t function without something else running first. [Service] — the actual behavior. Type=simple means systemd considers the service started as soon as it executes ExecStart — fine for a long-running foreground process like the example above. Restart=on-failure plus RestartSec=3 means a crash gets retried after 3 seconds instead of staying dead. [Install] — what happens when you enable it. WantedBy= says which target pulls this service in at boot/login. The gotcha that catches almost everyone once If you\u0026rsquo;re writing a user service (one that runs as your own user, not system-wide — placed in ~/.config/systemd/user/ and managed with systemctl --user), WantedBy=multi-user.target looks correct because that\u0026rsquo;s what every system-service example uses. It isn\u0026rsquo;t correct here. multi-user.target doesn\u0026rsquo;t exist in a user session — enabling against it produces:\nUnit /home/alan/.config/systemd/user/heartbeat-test.service is added as a dependency to a non-existent unit multi-user.target. The service still technically works when started manually, but it won\u0026rsquo;t actually enable correctly for auto-start. The fix, confirmed on a real system: use WantedBy=default.target for user services instead. System-wide services (the ones in /etc/systemd/system/, managed without --user) are the ones that correctly use multi-user.target.\nPutting it into place For a user service:\nmkdir -p ~/.config/systemd/user cp heartbeat.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now heartbeat.service For a system-wide service, the same commands without --user, placing the file in /etc/systemd/system/ instead, and prefixed with sudo.\nChecking it actually worked Two commands cover almost everything you need day to day:\nsystemctl --user status heartbeat.service # is it running, and since when journalctl --user -u heartbeat.service -f # live logs, -f to follow Drop --user for a system-wide service. journalctl -f in particular is worth knowing — it\u0026rsquo;s the systemd equivalent of tail -f for a service\u0026rsquo;s output, and it\u0026rsquo;s usually the fastest way to see why something didn\u0026rsquo;t start the way you expected.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/systemd-services-explained/","summary":"Every guide shows you a unit file to copy. Here\u0026rsquo;s what each line actually does, tested live, including the one gotcha that trips up nearly every first attempt at a user service.","title":"Systemd Services Explained: Writing Your Own Unit File"},{"content":"WSL2 isn\u0026rsquo;t a compatibility shim like the original WSL1 — it runs a real, lightweight Linux kernel in a managed VM, which means real filesystem semantics, real Docker support without the old workarounds, and a genuine apt/package manager environment. If you need Linux tooling for work but Windows for everything else, this is a better answer than dual-booting or running a full desktop VM.\n1. Install it with one command Modern Windows makes this almost boring, which is the point:\nwsl --install Run that from an administrator PowerShell or Command Prompt. It enables the required Windows features, downloads a default Linux distribution (Ubuntu, unless you specify otherwise), and sets up WSL2 as the default version. A reboot is typically required once.\nTo install a specific distribution instead of the default:\nwsl --install -d Debian Run wsl --list --online to see the full list of what\u0026rsquo;s available.\n2. Know where your files actually live The single most common WSL performance mistake: keeping your project files on the Windows filesystem (/mnt/c/...) and editing them from the Linux side. Cross-filesystem access between Windows and the WSL2 VM is slow — sometimes dramatically so for anything doing lots of small file operations (node_modules, git operations, build tools).\nKeep your actual working files inside the Linux filesystem itself (your Linux home directory, something like \\\\wsl$\\Ubuntu\\home\\yourname\\ if you need to reach it from Windows Explorer) and you avoid this entirely. Treat /mnt/c as a bridge for occasionally sharing files, not where your project lives day to day.\n3. Set resource limits before WSL sets them for you By default WSL2\u0026rsquo;s VM can use most of your system\u0026rsquo;s memory, which is fine until you\u0026rsquo;re running something memory-hungry on the Windows side simultaneously. Create (or edit) %UserProfile%\\.wslconfig:\n[wsl2] memory=8GB processors=4 swap=2GB Restart WSL for changes to take effect:\nwsl --shutdown 4. Use VS Code\u0026rsquo;s Remote-WSL extension, not a shared filesystem If you\u0026rsquo;re editing WSL-side files, don\u0026rsquo;t open them via the Windows path. Install the WSL extension in VS Code and open the folder from inside your WSL terminal with code . — this runs the actual VS Code server inside the Linux environment, giving you correct line endings, correct file permissions, and no cross-filesystem performance penalty.\n5. Know the actual limitations WSL2 is not a full Linux desktop replacement, even with WSLg (which does let GUI Linux apps run in windows on your Windows desktop now). GPU-heavy workloads, certain low-level hardware access, and anything expecting a full systemd init system historically needed extra configuration — systemd support has improved significantly in current WSL2 but isn\u0026rsquo;t universal across every distro image. If your workflow specifically needs full hardware access or a complete desktop environment, WSL2 is the wrong tool — that\u0026rsquo;s what a real Linux install or dual-boot is still for.\nWho this is actually for Developers who need Linux-native tooling (Docker, specific package managers, POSIX-behaving scripts) but whose day-to-day work — email, meetings, Windows-only software — happens on Windows. If you\u0026rsquo;re doing this to avoid learning Linux at all, WSL2 will still make you learn it; it\u0026rsquo;s just a much lower-friction way to start.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/wsl2-setup-guide/","summary":"You don\u0026rsquo;t need to dual-boot or run a VM to get a genuine Linux environment on Windows. Here\u0026rsquo;s what WSL2 actually gets you, and the handful of things worth configuring on day one.","title":"WSL2 Setup Guide: A Real Linux Environment on Windows Without Dual-Booting"},{"content":"There are hundreds of things you can self-host. Most people who try to run all of them burn out within a month. This is a shortlist organized by the problem each one solves, not an exhaustive directory.\nMedia: Jellyfin Open-source, no phone-home telemetry, no subscription tier gating features behind a paywall. Pairs well with the *arr stack (Sonarr/Radarr/Prowlarr) if you want automated library management, but Jellyfin alone is a complete, useful project on its own — start there before adding the automation layer.\nPhotos: Immich The most credible self-hosted alternative to a cloud photo backup service at this point — mobile app with automatic background upload, facial recognition, timeline view. This is one of the higher-value apps to self-host if you\u0026rsquo;re currently paying for cloud photo storage, since it directly replaces a recurring subscription.\nDNS-level ad blocking: Pi-hole or AdGuard Home Runs comfortably on the weakest hardware in your house. Blocks ads and trackers network-wide at the DNS level for every device, not just the one with a browser extension installed. Usually the first thing people self-host because the payoff is immediate and the resource cost is negligible.\nPassword manager: Vaultwarden A lightweight, self-hosted-friendly implementation of the Bitwarden server API — same official mobile/browser clients, your own server. Worth the extra care on backups given what\u0026rsquo;s stored in it (see the backup note in the old laptop to home server guide).\nFile sync/cloud storage: Nextcloud Heavier to run and maintain than the others on this list, but replaces Google Drive/Dropbox functionality reasonably completely, including calendar and contacts sync if you want to get off a cloud account entirely. Don\u0026rsquo;t start here if this is your first self-hosted app — start with Pi-hole or Jellyfin and work up to Nextcloud once you\u0026rsquo;re comfortable with updates and backups.\nA note on storage Jellyfin, Immich, and Nextcloud all have the same hidden cost: they\u0026rsquo;re only as useful as the storage behind them, and a laptop\u0026rsquo;s internal drive fills up fast once it\u0026rsquo;s holding a media library or a full photo backup. Two common ways to solve this: an external USB hard drive plugged straight into the server for the simplest setup, or a small dedicated NAS enclosure if you want storage that survives the server itself being replaced. Buy more than you think you need — media and photo libraries only grow. (Affiliate links — see the about page for the disclosure.)\nReverse proxy: Caddy or Traefik Not user-facing on its own, but once you\u0026rsquo;re running more than one or two services, a reverse proxy with automatic HTTPS certificates makes everything else easier to reach and secure. Caddy in particular is worth it for how little configuration it needs compared to nginx for the same result.\nPicking your first one If you only self-host one thing this month, make it Pi-hole or AdGuard Home — lowest resource cost, immediate and visible payoff, and it teaches you the Docker Compose workflow you\u0026rsquo;ll reuse for everything else on this list.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/self-hosted-apps-home-server/","summary":"A shortlist of self-hosted apps that are actually worth the maintenance burden, organized by what problem they solve.","title":"Best Self-Hosted Apps to Run on a Home Server in 2026"},{"content":"Most keybinding cheat sheets are a dump of a default config file, alphabetized and useless until you already know the system. This is grouped by intent, and — unlike most cheat sheets floating around — pulled directly from the live binding list on the machine instead of copied from an old blog post.\nGet your own live list first Don\u0026rsquo;t fully trust this page or any other. Omarchy prints your actual current bindings, including any personal overrides in ~/.config/hypr/bindings.lua, with:\nomarchy menu keybindings --print Everything below is the default set — if you\u0026rsquo;ve customized bindings, yours may differ.\nWindows Bind Action SUPER + W Close window CTRL + ALT + DELETE Close all windows SUPER + F Full screen SUPER + ALT + F Full width SUPER + T Toggle floating/tiling SUPER + J Toggle window split SUPER + O Pop window out (float \u0026amp; pin) Workspaces Bind Action SUPER + [0-9] Switch to workspace [0-9] (0 = workspace 10) SUPER + SHIFT + [0-9] Move window to workspace, and switch to it SUPER + SHIFT + ALT + [0-9] Move window to workspace silently (stay put) SUPER + TAB Next workspace SUPER + SHIFT + TAB Previous workspace SUPER + CTRL + TAB Former workspace (jump back) The silent-move bind (SUPER+SHIFT+ALT+N) is the one people miss and then miss again once they learn it exists — it\u0026rsquo;s the difference between organizing windows and losing your current focus every time you file something away.\nLaunching \u0026amp; system Bind Action SUPER + RETURN Terminal SUPER + SHIFT + RETURN Browser SUPER + SHIFT + ALT + B Browser (private) SUPER + SHIFT + F File manager SUPER + SHIFT + ALT + F File manager (current working dir) SUPER + SPACE Omarchy menu SUPER + ESCAPE System menu SUPER + K Show keybindings SUPER + CTRL + L Lock system SUPER + SHIFT + CTRL + SPACE Theme menu Copy/paste/screenshots (the ones people forget exist) Bind Action SUPER + C / V / X Universal copy / paste / cut SUPER + CTRL + V Clipboard manager SUPER + CTRL + E Emojis SUPER + PRINT Color picker PRINT Screenshot ALT + PRINT Screen recording The ones worth memorizing first If you memorize nothing else in week one: SUPER+RETURN (terminal), workspace switching (SUPER+[0-9]), and SUPER+W (close window). That covers most daily navigation. Everything else you\u0026rsquo;ll pick up by needing it once and running omarchy menu keybindings --print to find it — don\u0026rsquo;t try to memorize the whole list on day one.\nWant this whole reference offline, plus a few config tweaks (floating picture-in-picture, tuned touchpad, extra binds) applied automatically with a backup first? That\u0026rsquo;s the Omarchy Power Pack — optional, everything in it is also just written out for free across the guides on this site.\nOnce the desktop side feels comfortable, a natural next project is turning a spare machine into a home server — same terminal-first workflow, just pointed at something that runs 24/7 instead of your daily driver.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/hyprland-keybindings-cheat-sheet/","summary":"The core Omarchy binds worth memorizing first, organized by what you\u0026rsquo;re actually trying to do — pulled from the live binding list, not a copy-pasted default config.","title":"Omarchy Keybindings Cheat Sheet You'll Actually Use"},{"content":"Omarchy (Arch Linux + Hyprland, preconfigured by DHH\u0026rsquo;s install script) is deliberately opinionated: it hands you a working tiling setup instead of a blank Arch install and a weekend of dotfiles archaeology. That\u0026rsquo;s the whole pitch. But \u0026ldquo;opinionated\u0026rdquo; means some defaults will fight your workflow until you change a handful of things.\n1. Know the config is Lua now, not plain Hyprland conf As of the 4.x line, ~/.config/hypr/ holds .lua files, not .conf. Your entry point is hyprland.lua, which loads Omarchy\u0026rsquo;s own defaults first (require(\u0026quot;default.hypr.omarchy\u0026quot;)) and then your personal override files in order:\nrequire(\u0026#34;hypr.monitors\u0026#34;) require(\u0026#34;hypr.input\u0026#34;) require(\u0026#34;hypr.bindings\u0026#34;) require(\u0026#34;hypr.looknfeel\u0026#34;) require(\u0026#34;hypr.autostart\u0026#34;) The key idea: you never edit Omarchy\u0026rsquo;s defaults directly. You add or unbind things in your own files, which are loaded after the defaults — so package updates can improve Omarchy\u0026rsquo;s base config without ever touching or conflicting with your personal changes. Resist the urge to go copy the whole default config into your override files \u0026ldquo;to see everything\u0026rdquo; — that defeats the point and turns future updates into a manual merge chore.\n2. See what you actually have before changing anything Two commands answer almost every \u0026ldquo;how do I\u0026hellip;\u0026rdquo; question before you touch a config file:\nomarchy menu keybindings --print # every active keybinding, including your overrides hyprctl monitors # connected displays, names, resolutions Run both before editing anything. The keybindings list in particular is generated live, so it\u0026rsquo;s always accurate — trust it over any cheat sheet (including the one on this site) if they ever disagree.\n3. Set your monitor layout explicitly if you run more than one display Get exact monitor names from hyprctl monitors, then set explicit positions, refresh rate, and scale in ~/.config/hypr/monitors.lua. This alone eliminates most of the \u0026ldquo;window opened on the wrong screen\u0026rdquo; annoyance in the first week with a multi-monitor setup.\n4. Add bindings, don\u0026rsquo;t fight the defaults ~/.config/hypr/bindings.lua ships mostly commented-out examples on a fresh install — you add to it, you don\u0026rsquo;t need to rewrite it. The pattern for adding a new bind:\no.bind(\u0026#34;SUPER + SHIFT + R\u0026#34;, \u0026#34;SSH\u0026#34;, \u0026#34;alacritty -e ssh your-server\u0026#34;) (If your-server doesn\u0026rsquo;t exist yet, see turning a spare laptop into a home server — cheapest way to get something worth SSH-ing into.)\nAnd to change something Omarchy already binds, unbind first, then bind again — don\u0026rsquo;t just add a second binding on top of the first:\nhl.unbind(\u0026#34;SUPER + SPACE\u0026#34;) o.bind(\u0026#34;SUPER + SPACE\u0026#34;, \u0026#34;Omarchy menu\u0026#34;, \u0026#34;omarchy-menu toggle root\u0026#34;) If you want a clean slate instead of layering on top of the defaults, set omarchy_default_bindings = false in hyprland.lua before the require(\u0026quot;default.hypr.omarchy\u0026quot;) line, then define everything yourself in bindings.lua. This is a bigger commitment than most people need in week one.\n5. Notice the bar isn\u0026rsquo;t waybar Current Omarchy renders its bar/menu system through quickshell (the process shows up as omarchy-shell in ps aux), not waybar. If you\u0026rsquo;re coming from a manual Hyprland setup and go looking for a waybar config to tweak, it isn\u0026rsquo;t there — bar customization goes through Omarchy\u0026rsquo;s own shell config, not a waybar config.jsonc.\nWhat to leave alone The keybinding scheme and workspace behavior are well thought through and consistent with each other (e.g. SUPER+SHIFT+[1-9] moves a window to a workspace, SUPER+SHIFT+ALT+[1-9] does it silently without switching focus there). Don\u0026rsquo;t rebind everything to match your old window manager on day one — give the defaults a real week using omarchy menu keybindings --print as your reference. Most people who do this end up keeping the majority of them.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/omarchy-setup-guide/","summary":"Omarchy ships with strong opinionated defaults, configured through a Lua API rather than plain Hyprland conf files. Here\u0026rsquo;s what\u0026rsquo;s worth touching immediately, and what to leave alone.","title":"Omarchy Setup Guide: What to Change in Your First Hour"},{"content":"This site covers five areas: Linux, Windows, Gaming, Self-Hosting, and Programming. Pick the one you\u0026rsquo;re actually here for — no need to read this site front to back.\nIf you just installed Omarchy Omarchy Setup Guide — the handful of things worth changing in your first hour, before you build habits around the defaults. Hyprland Keybindings Cheat Sheet — print this or keep it in a tab until the binds are muscle memory. Turn an Old Laptop Into a Home Server — once your desktop feels good, this is the natural next project. Best Self-Hosted Apps for a Home Server — what to actually put on that server once it\u0026rsquo;s up. If you\u0026rsquo;re on Windows but want Linux tooling WSL2 Setup Guide — a real Linux environment without dual-booting, and the handful of things worth configuring on day one. If you game on Linux, or are considering it Gaming on Linux in 2026 — what Proton actually handles, what still needs checking first, and the tools that matter. If you write PHP Laravel to Craft CMS — what actually transfers between the two, and what will quietly cost you time if you assume it does. More guides get added as they\u0026rsquo;re tested for real, not written speculatively — check the posts index or browse by category for everything published so far.\nWant the Omarchy config pre-packaged instead of typed by hand? The touchpad tuning, window rules, and extra keybindings covered in the Omarchy guides above are also available as a ready-to-run installer — the Omarchy Power Pack. It backs up your ~/.config/hypr before touching anything and lets you apply each module individually, plus it includes the full 224-bind keybinding reference offline. Entirely optional — everything in it is also just written out in the guides above for free if you\u0026rsquo;d rather type it yourself.\nWriting PHP/Laravel and miss Artisan\u0026rsquo;s factories? Sprout brings that exact workflow to Craft CMS — covered in more detail in the Laravel to Craft CMS post above.\n","permalink":"https://alanone.github.io/undefined-behavior/posts/start-here/","summary":"Where to start depending on what you\u0026rsquo;re actually here for — organized by category, not just publish order.","title":"Start Here: A Practical Path Through This Site"},{"content":"The laptop that\u0026rsquo;s too slow for daily use is usually not too slow to run Jellyfin, Pi-hole, or a handful of small self-hosted apps 24/7. That\u0026rsquo;s the whole appeal: you already own the hardware, and running it as a server converts a drawer object into something producing value continuously.\n1. Strip it down to a headless setup Close the lid without it sleeping — edit the lid-switch behavior in your power manager config (logind.conf if you\u0026rsquo;re on systemd: set HandleLidSwitch=ignore) so the machine keeps running lid-closed. Disable the display output entirely once you\u0026rsquo;ve confirmed SSH access works; a dead/cracked screen is fine for this use case, you\u0026rsquo;ll never look at it again.\n2. Give it a static local IP Set a DHCP reservation in your router for the laptop\u0026rsquo;s MAC address, or configure a static IP directly on the machine. Everything downstream — SSH, reverse proxy rules, app configs — gets much more annoying to maintain if the IP can silently change after a reboot.\nIf the laptop only has WiFi, get it on wired ethernet before going further — WiFi is the single most common source of mystery flakiness on a server nobody\u0026rsquo;s actively watching. A cheap USB-to-Ethernet adapter solves this on machines without a built-in port. (Affiliate link, see the about page.)\n3. Lock down SSH before anything else This machine is about to run continuously and be reachable on your network. Before installing a single app:\nsudo systemctl disable --now sshd # temporarily, while you configure it Then set up key-based auth, disable password login in sshd_config (PasswordAuthentication no), and only re-enable the service once that\u0026rsquo;s confirmed working from another machine. Skipping this step is the single most common regret people report after a few months of running a home server.\n4. Install Docker and use Compose from day one Even for a single-app server, Docker Compose gives you a reproducible, documented config instead of a pile of manually-installed packages you\u0026rsquo;ll forget how to reproduce:\nsudo pacman -S docker docker-compose sudo systemctl enable --now docker Keep one docker-compose.yml per app in its own directory, or one big compose file with multiple services — either works, but write it down instead of running ad-hoc docker run commands you won\u0026rsquo;t remember in six months.\n5. Pick your first app, not your eventual stack Don\u0026rsquo;t try to stand up ten services on day one. Start with one you\u0026rsquo;ll actually use daily — see the self-hosted apps guide for a shortlist — get comfortable with backups and updates for that one app, then add the next.\n6. Set up backups before you have anything worth losing Whatever you self-host, decide the backup story before you have six months of photos or notes in it. A cron job that rsyncs the app\u0026rsquo;s data directory to another machine or cloud storage nightly is a fine starting point — don\u0026rsquo;t let \u0026ldquo;I\u0026rsquo;ll set up backups later\u0026rdquo; become the default.\nWorth pairing with a small UPS — a laptop with its battery still installed already has some protection against a dirty shutdown, but if you\u0026rsquo;ve stripped the battery or you\u0026rsquo;re running different hardware, a power blip mid-write is a genuinely common way to corrupt whatever database or file your app was touching at that exact moment. (Affiliate link, see the about page.)\nWhat this actually costs to run An old laptop idling with a handful of lightweight containers typically draws well under 20W. At most electricity rates that\u0026rsquo;s a few dollars a month — cheap enough that the real constraint is your time, not the power bill.\nNo spare laptop lying around? Everything above assumes you already own hardware worth repurposing — that\u0026rsquo;s the whole appeal, it\u0026rsquo;s free. If you don\u0026rsquo;t have a spare machine, or specifically want something reachable from outside your home network without exposing your home IP or fighting your ISP\u0026rsquo;s NAT, a small cloud VPS is the paid equivalent: same Docker Compose workflow from step 4 onward, just running on rented hardware instead of your own. DigitalOcean is a common starting point for this — their cheapest droplet is plenty for the same Pi-hole/Vaultwarden-scale workload this guide covers. (That\u0026rsquo;s a referral link — it gets you a signup credit and costs you nothing extra; see the about page for the full disclosure.)\n","permalink":"https://alanone.github.io/undefined-behavior/posts/old-laptop-to-home-server/","summary":"You don\u0026rsquo;t need a NAS or a rack to start self-hosting. An old laptop with a broken screen and a wall outlet is enough.","title":"Turn an Old Laptop Into a Home Server (Step by Step)"},{"content":"Undefined Behavior is a small, honest collection of notes on Linux, Windows, gaming, self-hosting, and programming. No sponsored fluff, no AI-generated listicles — just what actually happened when setting this stuff up for real.\nThe name is the point: this is what you get when you push a system past what it\u0026rsquo;s officially supposed to do — the config that isn\u0026rsquo;t documented, the compatibility edge case, the framework instinct that doesn\u0026rsquo;t transfer. That\u0026rsquo;s the whole beat of this site.\nEverything here is either tested directly on a real machine, or carefully researched and flagged for exactly that reason before it goes live — the two are never presented as the same thing. If a post says something was verified hands-on, it was.\nSome links on this site are affiliate/referral links to tools and services mentioned in the guides. If you use one, it costs you nothing extra and helps keep this site running. Nothing gets linked just because it pays.\n","permalink":"https://alanone.github.io/undefined-behavior/about/","summary":"\u003cp\u003eUndefined Behavior is a small, honest collection of notes on Linux, Windows, gaming,\nself-hosting, and programming. No sponsored fluff, no AI-generated listicles — just what\nactually happened when setting this stuff up for real.\u003c/p\u003e\n\u003cp\u003eThe name is the point: this is what you get when you push a system past what it\u0026rsquo;s\nofficially supposed to do — the config that isn\u0026rsquo;t documented, the compatibility edge case,\nthe framework instinct that doesn\u0026rsquo;t transfer. That\u0026rsquo;s the whole beat of this site.\u003c/p\u003e","title":"About"}]