{"id":816,"date":"2026-06-14T21:48:20","date_gmt":"2026-06-14T16:18:20","guid":{"rendered":"https:\/\/www.cyberaka.com\/?p=816"},"modified":"2026-06-14T21:48:21","modified_gmt":"2026-06-14T16:18:21","slug":"how-i-turned-a-thrown-away-laptop-into-a-private-cloud-server-and-replaced-a-saas-in-the-process","status":"publish","type":"post","link":"https:\/\/www.cyberaka.com\/?p=816","title":{"rendered":"How I Turned a Thrown-Away Laptop Into a Private Cloud Server (And Replaced a SaaS in the Process)"},"content":{"rendered":"<p><!-- WordPress: paste into a Custom HTML block, or switch the Classic Editor to the \"Text\" tab. Set the post title separately in WordPress. --><\/p>\n<p><em>A solopreneur&#8217;s story of \u20b90 infrastructure, WireGuard tunnels, and the laptop nobody wanted.<\/em><\/p>\n<hr \/>\n<p>There&#8217;s a 2014 HP Notebook sitting on my desk right now. Intel i5-4210U, 16GB RAM, 238GB SSD. Until this morning it was doing absolutely nothing \u2014 the kind of machine that&#8217;s too old to be your daily driver but too functional to throw out. You know the one. Maybe you have five of them, like I do.<\/p>\n<p>Today that laptop became a server.<\/p>\n<p>It&#8217;s running OpenObserve, an open-source observability platform. It&#8217;s reachable from anywhere in the world through an encrypted WireGuard tunnel. I can RDP into its desktop from my Mac via my Linode instance in the US. And it replaced Better Stack \u2014 a SaaS I was paying for to ingest logs from quizwrap.com.<\/p>\n<p>Total additional spend: \u20b90.<\/p>\n<p>This is the story of how I got there, why I did it, and what it unlocked that I didn&#8217;t expect.<\/p>\n<hr \/>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_85 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<label for=\"ez-toc-cssicon-toggle-item-6a2f0b8fe973a\" class=\"ez-toc-cssicon-toggle-label\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/label><input type=\"checkbox\"  id=\"ez-toc-cssicon-toggle-item-6a2f0b8fe973a\"  aria-label=\"Toggle\" \/><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Problem_With_Paying_for_Things_You_Already_Own\" >The Problem With Paying for Things You Already Own<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Stack_I_Chose\" >The Stack I Chose<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Laptop_Nobody_Wanted\" >The Laptop Nobody Wanted<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#WireGuard_The_Networking_Problem_That_Wasnt\" >WireGuard: The Networking Problem That Wasn&#8217;t<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#OpenObserve_in_Five_Minutes\" >OpenObserve in Five Minutes<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Unexpected_Unlock_RDP_From_Anywhere\" >The Unexpected Unlock: RDP From Anywhere<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#%E2%80%9CIt_Posts_200s%E2%80%9D_Is_Not_%E2%80%9CIts_Done%E2%80%9D\" >&#8220;It Posts 200s&#8221; Is Not &#8220;It&#8217;s Done&#8221;<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#Raw_lines_are_useless_parse_at_the_edge\" >Raw lines are useless; parse at the edge<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#A_service_that_dies_when_you_close_the_terminal_isnt_a_service\" >A service that dies when you close the terminal isn&#8217;t a service<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#Surviving_a_reboot_hands-off\" >Surviving a reboot, hands-off<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_rotation_I_chose_not_to_do\" >The rotation I chose not to do<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Philosophy_Spare_Compute_Is_a_Private_Cloud\" >The Philosophy: Spare Compute Is a Private Cloud<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#What_Id_Do_Differently\" >What I&#8217;d Do Differently<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Current_State_and_Whats_Next\" >The Current State and What&#8217;s Next<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/www.cyberaka.com\/?p=816\/#The_Stack_For_Reference\" >The Stack, For Reference<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"The_Problem_With_Paying_for_Things_You_Already_Own\"><\/span>The Problem With Paying for Things You Already Own<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I run quizwrap.com \u2014 an educational quiz platform that&#8217;s organically grown to 314 post-launch users across 60 countries and 28 institutions \u2014 entirely without paid acquisition. I&#8217;m a solopreneur. I run everything on a single Linode server. I watch costs carefully.<\/p>\n<p>Better Stack was doing log ingestion for me on their free tier. It worked well \u2014 clean UI, SQL queries, good retention. I have nothing against them; it&#8217;s a solid product. Then this morning I got an email: <em>&#8220;Your organization cyberaka.com is at 80% of your plan quota. Any new data will be dropped once you hit the limit.&#8221;<\/em> I clicked through to the upgrade page. The entry-level paid plan starts at $38\/month.<\/p>\n<p>I closed the tab.<\/p>\n<p>To put that number in context: I pay $72\/month for an 8GB dedicated Linode that runs five production websites. Being asked to pay half my entire server bill just for log ingestion \u2014 on top of everything else \u2014 didn&#8217;t make sense. Better Stack is a great product for teams that need it. I&#8217;m a solopreneur who needed to do the math.<\/p>\n<p>I had compute sitting idle at home. I had a public-IP Linode that could act as a coordination hub. The only thing I lacked was the motivation to set it up. The upgrade prompt provided it.<\/p>\n<p>Log ingestion is two things: a shipper that collects logs and sends them somewhere, and a store that holds and queries them. Better Stack bundled both behind a nice UI. But the open-source world has perfectly good equivalents \u2014 I just needed to wire them together.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"The_Stack_I_Chose\"><\/span>The Stack I Chose<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>After evaluating options, I landed on:<\/p>\n<ul>\n<li><strong>OpenObserve<\/strong> \u2014 single binary (or container), SQL-based querying, built-in UI, runs on 512MB RAM. The closest open-source match to the Better Stack experience.<\/li>\n<li><strong>Vector<\/strong> \u2014 log shipper from the folks at Datadog (open-source). Reads nginx access and error logs and ships them over HTTP.<\/li>\n<li><strong>WireGuard<\/strong> \u2014 encrypted peer-to-peer tunnel. My Linode is the hub; the laptop is a peer.<\/li>\n<li><strong>Docker Desktop on Windows<\/strong> \u2014 to run OpenObserve as a container with persistent storage.<\/li>\n<\/ul>\n<p>The architecture is simple:<\/p>\n<pre><code>quizwrap.com traffic\n       \u2193\n   Nginx on Linode\n       \u2193\n   Vector (ships logs over HTTP)\n       \u2193\n   WireGuard encrypted tunnel\n       \u2193\n   OpenObserve on old HP laptop at home\n       \u2193\n   Dashboard in browser<\/code><\/pre>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"The_Laptop_Nobody_Wanted\"><\/span>The Laptop Nobody Wanted<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let me tell you about this HP Notebook. BIOS date: May 2016. Factory-installed OS: Windows 10. It had been sitting unused because the internal Wi-Fi chip had died \u2014 <code>Hardware not present<\/code> in Device Manager. I&#8217;d plugged a TP-Link USB Wi-Fi dongle into it as a workaround, which is the kind of solution that works until it doesn&#8217;t.<\/p>\n<p>Before I could use it as a server, I needed to fix a few things:<\/p>\n<p><strong>First: enable Intel VT-x in BIOS.<\/strong> The HP Insyde BIOS had virtualization disabled by default \u2014 their own help text says &#8220;HP recommends this feature remain disabled unless specialized applications are being used.&#8221; Docker Desktop needs VT-x. Into the BIOS I went (F10 on boot), navigated to System Configuration, toggled Virtualization Technology from Disabled to Enabled. One reboot.<\/p>\n<p><strong>Second: install WSL2 and Docker Desktop.<\/strong> With VT-x now enabled, <code>wsl --install<\/code> worked cleanly. Docker Desktop 4.77.0 installed with the WSL2 backend. The first launch timed out \u2014 a known issue on older Haswell hardware where the WSL2 VM takes longer to initialise than Docker&#8217;s default timeout. Running <code>wsl --shutdown<\/code> and relaunching Docker Desktop a second time fixed it. <code>docker run hello-world<\/code> printed its success message.<\/p>\n<p><strong>Third: kill sleep forever.<\/strong> A server cannot sleep. Power &amp; Sleep \u2192 all dropdowns to Never. <code>powercfg \/hibernate off<\/code> in PowerShell. Control Panel \u2192 Power Options \u2192 lid close action \u2192 Do nothing on both battery and plugged in. This is the step most people forget, and it&#8217;s the one that kills your uptime at 3am when nobody&#8217;s watching.<\/p>\n<p><strong>Fourth: plug in ethernet.<\/strong> The USB Wi-Fi dongle is a single point of failure. I have two internet connections at home. The fast one via ethernet \u2014 now plugged directly into the laptop. The USB dongle stays connected for local network RDP access within the home. Two separate interfaces, two separate purposes, route metrics set so ethernet wins for internet traffic.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"WireGuard_The_Networking_Problem_That_Wasnt\"><\/span>WireGuard: The Networking Problem That Wasn&#8217;t<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The obvious challenge with running a server at home is that your laptop is behind NAT. It has no public IP. Port-forwarding your home router to expose port 5080 to the internet is both fragile (dynamic home IPs) and a security liability.<\/p>\n<p>WireGuard solves this elegantly. The Linode \u2014 which has a static public IP \u2014 becomes the hub. The home laptop dials <em>outbound<\/em> to the Linode on UDP 51820. Because the connection is initiated from the laptop side, it punches through NAT without any port forwarding. <code>PersistentKeepalive = 25<\/code> keeps the NAT mapping open so the Linode can initiate connections back through it.<\/p>\n<p>The setup:<\/p>\n<ul>\n<li>Linode gets tunnel IP <code>10.10.0.1<\/code><\/li>\n<li>Laptop gets tunnel IP <code>10.10.0.2<\/code><\/li>\n<li>OpenObserve listens on <code>10.10.0.2:5080<\/code><\/li>\n<li>Vector on Linode ships logs to <code>http:\/\/10.10.0.2:5080<\/code><\/li>\n<\/ul>\n<p>Key generation on Linode:<\/p>\n<pre><code>wg genkey | sudo tee \/etc\/wireguard\/linode_private.key | \\\n  wg pubkey | sudo tee \/etc\/wireguard\/linode_public.key<\/code><\/pre>\n<p>The Windows WireGuard GUI generates its own keypair \u2014 just add the config and activate. Within seconds: <code>Latest handshake: 14 seconds ago. Transfer: 92 B received, 180 B sent.<\/code> Tunnel confirmed.<\/p>\n<p>One gotcha: my Linode has a cloud firewall managed at the dashboard level, not via ufw on the box. I&#8217;d opened ports there for SSH, HTTP, and HTTPS \u2014 but not WireGuard&#8217;s UDP 51820. Add that rule and save, or handshakes never complete. The Linode dashboard shows a pending indicator (\u21ba) until you hit Save Changes \u2014 easy to miss.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"OpenObserve_in_Five_Minutes\"><\/span>OpenObserve in Five Minutes<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<pre><code>version: '3.3'\nservices:\n  openobserve:\n    image: public.ecr.aws\/zinclabs\/openobserve:latest\n    container_name: openobserve\n    restart: always\n    environment:\n      - ZO_ROOT_USER_EMAIL=your@email.com\n      - ZO_ROOT_USER_PASSWORD=yourpassword\n    ports:\n      - \"5080:5080\"\n    volumes:\n      - C:\\openobserve\\data:\/data<\/code><\/pre>\n<p><code>docker compose up -d<\/code> and thirty seconds later <code>http:\/\/localhost:5080<\/code> shows the OpenObserve home screen: <em>No Data Ingested<\/em>. Exactly right. The pipeline just needs to be connected.<\/p>\n<p>One follow-up command worth running immediately:<\/p>\n<pre><code>docker update --restart always openobserve<\/code><\/pre>\n<p>This ensures OpenObserve restarts automatically after a Windows Update reboot, without needing you to manually run docker compose up.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"The_Unexpected_Unlock_RDP_From_Anywhere\"><\/span>The Unexpected Unlock: RDP From Anywhere<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Here&#8217;s the thing I didn&#8217;t plan for but got for free.<\/p>\n<p>Once you have WireGuard connecting your Linode to your home laptop, you can use that same tunnel for anything TCP. Including RDP.<\/p>\n<p>One command on your Mac:<\/p>\n<pre><code>ssh -L 3389:10.10.0.2:3389 -L 5080:10.10.0.2:5080 your-user@your-domain.com -N<\/code><\/pre>\n<p>This creates two local port forwards over the SSH connection:<\/p>\n<ul>\n<li><code>localhost:3389<\/code> \u2192 Windows laptop RDP (via WireGuard tunnel)<\/li>\n<li><code>localhost:5080<\/code> \u2192 OpenObserve dashboard (via WireGuard tunnel)<\/li>\n<\/ul>\n<p>Open Microsoft&#8217;s Windows App (the new name for Microsoft Remote Desktop), connect to <code>localhost<\/code>, enter Windows credentials \u2014 and you&#8217;re looking at the desktop of a laptop sitting in your home, tunnelled through a server in the US, from wherever you are in the world.<\/p>\n<p>The security picture is clean. Port 3389 is not exposed to the internet at all. Only traffic that arrives through your SSH tunnel (protected by your SSH key) can reach the WireGuard tunnel, which then reaches the laptop. There&#8217;s no attack surface for random scanners.<\/p>\n<p>One prerequisite I&#8217;d missed: <code>AllowTcpForwarding<\/code> in <code>\/etc\/ssh\/sshd_config<\/code> was set to <code>no<\/code> on the Linode. The error <code>channel 2: open failed: administratively prohibited<\/code> tells you exactly what&#8217;s wrong once you know what to look for. Set it to <code>yes<\/code>, <code>sudo systemctl restart ssh<\/code>, done.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"%E2%80%9CIt_Posts_200s%E2%80%9D_Is_Not_%E2%80%9CIts_Done%E2%80%9D\"><\/span>&#8220;It Posts 200s&#8221; Is Not &#8220;It&#8217;s Done&#8221;<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When I came back to wire up the last piece \u2014 Vector actually shipping to OpenObserve \u2014 I found it was already working. <code>docker logs openobserve<\/code> had the smoking gun:<\/p>\n<pre><code>POST \/api\/default\/quizwrap\/_json HTTP\/1.1\" 200 ... \"Vector\/0.56.0\"<\/code><\/pre>\n<p>Logs were arriving. Pipeline complete, right? Not even close. &#8220;Bytes are landing in the database&#8221; is the demo. The gap between that and something I&#8217;d actually rely on took longer than everything before it \u2014 and it&#8217;s the part nobody blogs about.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Raw_lines_are_useless_parse_at_the_edge\"><\/span>Raw lines are useless; parse at the edge<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The logs landing in OpenObserve were raw nginx strings stuffed into a single <code>message<\/code> field:<\/p>\n<pre><code>104.22.24.138 - - [14\/Jun\/2026:12:18:05 +0000] \"GET \/ HTTP\/1.1\" 200 1978 \"-\" \"curl\/8.5.0\"<\/code><\/pre>\n<p>You can full-text search that, but you can&#8217;t ask the questions that matter: <em>show me every 5xx<\/em>, <em>which IPs are hammering \/api<\/em>, <em>what&#8217;s my 404 rate<\/em>. For that you need structured fields \u2014 and the cleanest place to create them is at the shipper, before they ever hit storage.<\/p>\n<p>Vector has a built-in VRL function for exactly this. I dropped a <code>remap<\/code> transform between the file source and the HTTP sink:<\/p>\n<pre><code>transforms:\n  parse_access:\n    type: remap\n    inputs: [nginx_access]\n    source: |\n      parsed, err = parse_nginx_log(.message, \"combined\")\n      if err == null {\n          . = merge(., parsed)\n          .log_type = \"nginx_access\"\n      } else {\n          .log_type = \"nginx_access_unparsed\"\n          .parse_error = err\n      }<\/code><\/pre>\n<p>The <code>if err == null<\/code> branch matters: a malformed line still gets shipped (tagged <code>_unparsed<\/code>) instead of silently vanishing, and the raw text stays in <code>message<\/code>, so nothing is ever lost. Same pattern for the error log with format <code>\"error\"<\/code>.<\/p>\n<p>After that, the same request arrives as:<\/p>\n<pre><code>{ \"client\": \"104.22.24.138\", \"status\": 200, \"request\": \"GET \/ HTTP\/1.1\",\n  \"size\": 1978, \"agent\": \"curl\/8.5.0\", \"log_type\": \"nginx_access\" }<\/code><\/pre>\n<p>Now <code>status &gt;= 500<\/code> is a query, not a grep.<\/p>\n<p>And the payoff was immediate. Within minutes of structured logs flowing, I spotted this in the stream:<\/p>\n<pre><code>GET \/.git\/refs\/heads\/main  \u2192 404\nGET \/.env.orig             \u2192 404\nPROPFIND \/                 \u2192 405<\/code><\/pre>\n<p>Bots, constantly probing for an exposed git repo or a leaked secrets file. All 404&#8217;d \u2014 nothing leaked \u2014 but I&#8217;d never have caught the pattern in a wall of raw text. The observability I built to save money immediately paid off as a security signal.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"A_service_that_dies_when_you_close_the_terminal_isnt_a_service\"><\/span>A service that dies when you close the terminal isn&#8217;t a service<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Vector was running only because I&#8217;d typed <code>vector --config ...<\/code> into an SSH session. The moment that session closed \u2014 or the Linode rebooted \u2014 logging would stop, silently. The fix is a systemd unit:<\/p>\n<pre><code>[Service]\nType=exec\nUser=cyberaka\nExecStart=\/home\/cyberaka\/.vector\/bin\/vector --config \/home\/cyberaka\/.vector\/config\/vector.yaml\nRestart=always\nRestartSec=10<\/code><\/pre>\n<p><code>systemctl enable --now vector<\/code> and it now starts on boot, restarts on crash, and survives me disconnecting.<\/p>\n<p>But there was a trap I nearly walked into. My setup notes said to clear the old process with <code>pkill -f vector<\/code>. Before running it I checked what was actually running \u2014 and found two <em>other<\/em> <code>vector<\/code> processes owned by root, with a binary path that didn&#8217;t even exist on disk. For a second I thought the box was compromised. They turned out to be Vector instances running <em>inside other Docker containers<\/em> (a staging and prod telemetry stack I&#8217;d forgotten about) \u2014 the binary &#8220;didn&#8217;t exist&#8221; because it lived inside the container&#8217;s filesystem namespace. A blanket <code>pkill -f vector<\/code> would have killed those too.<\/p>\n<p>Lesson: never pattern-kill on a name as generic as &#8220;vector.&#8221; I scoped the kill to the exact config path \u2014 <code>pkill -f 'vector --config \/home\/cyberaka\/...'<\/code> \u2014 so it could only ever match the one process I meant.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Surviving_a_reboot_hands-off\"><\/span>Surviving a reboot, hands-off<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>This was the real test, and the first reboot failed it \u2014 usefully. After restarting the laptop the tunnel was up (the WireGuard service started fine) but logs stopped. The culprit: <strong>Docker Desktop is a user application, not a boot service.<\/strong> It doesn&#8217;t start until someone logs into Windows, and the OpenObserve container can&#8217;t restart if Docker itself isn&#8217;t running.<\/p>\n<p>The full hands-off chain needed three independent things to auto-start:<\/p>\n<ol>\n<li><strong>WireGuard tunnel<\/strong> \u2014 installed as a Windows service so it comes up <em>before<\/em> login:\n<pre><code>wireguard \/installtunnelservice \"C:\\wireguard\\linode-hub.conf\"<\/code><\/pre>\n<p>(Start type <code>Automatic<\/code>, confirmed with <code>Get-Service<\/code>.)<\/li>\n<li><strong>Docker Desktop<\/strong> \u2014 Settings \u2192 General \u2192 <em>Start Docker Desktop when you sign in<\/em>. The container&#8217;s <code>restart: always<\/code> then brings OpenObserve back on its own.<\/li>\n<li><strong>Windows auto-login<\/strong> \u2014 the piece that closes the loop. Because Docker needs a login, an unattended box has to log <em>itself<\/em> in. <code>netplwiz<\/code> \u2192 uncheck <em>Users must enter a user name and password<\/em>. Now a power-cut at 3am recovers without me.<\/li>\n<\/ol>\n<p>The chain after any reboot:<\/p>\n<blockquote>\n<p>power on \u2192 Windows auto-logs-in \u2192 Docker Desktop starts \u2192 OpenObserve container restarts \u2192 WireGuard already up \u2192 Vector ships \u2192 logs flow<\/p>\n<\/blockquote>\n<p>I tested it the dumb, honest way: rebooted and walked away. Two minutes later, querying from the Linode, fresh logs were landing. I even shut the lid (lid action set to <em>Do nothing<\/em>) to confirm it keeps serving with the screen closed \u2014 it does.<\/p>\n<p>In practice, though, I run it lid-<em>open<\/em>. A laptop running 24\/7 sheds heat far better with the deck exposed than clamshelled with the screen trapping it on top. On a 15W i5 under light load the difference is small, but it&#8217;s free: lid open, on a hard flat surface (never carpet or a bed \u2014 that blocks the bottom air intake, the number-one cause of laptop overheating), slightly elevated for airflow, and the display set to sleep on idle so the panel isn&#8217;t burning power for no one. It&#8217;s sitting on a shelf now, and it just works.<\/p>\n<p>One quiet hero in all this: Vector&#8217;s <strong>disk buffer<\/strong>. While OpenObserve was down during that failed first reboot, Vector didn&#8217;t drop anything \u2014 it spooled logs to a 256 MB on-disk buffer (<code>when_full: block<\/code>) and flushed the backlog the instant the tunnel came back. The outage cost me zero log lines.<\/p>\n<p>And one last knob so the SSD never fills: I capped retention at 45 days on the stream. Old data gets compacted away automatically; disk usage stays bounded forever.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"The_rotation_I_chose_not_to_do\"><\/span>The rotation I chose <em>not<\/em> to do<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A couple of secrets \u2014 the OpenObserve password, a WireGuard key \u2014 passed through notes and terminals during setup. The reflexive security advice is &#8220;rotate everything.&#8221; I didn&#8217;t, and I think that&#8217;s the right call: OpenObserve is never exposed to the internet (only reachable over the tunnel and my LAN), and this is a single-user home lab. The threat model doesn&#8217;t justify the churn. What <em>does<\/em> matter \u2014 and what I did verify \u2014 is that the OpenObserve password isn&#8217;t reused on anything internet-facing. Security is a math problem too; spend the effort where the threat is real.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"The_Philosophy_Spare_Compute_Is_a_Private_Cloud\"><\/span>The Philosophy: Spare Compute Is a Private Cloud<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I have five old laptops at home. Until today they were liabilities \u2014 things to dust, to not throw away, to feel vaguely guilty about not using.<\/p>\n<p>The mental model shift is this: <strong>treat spare home compute as tier-2 infrastructure, not as failed hardware.<\/strong><\/p>\n<p>Your Linode (or any VPS) is tier-1: always-on, public-facing, static IP, runs your websites and anything customer-facing. The home laptops are tier-2: always-on internally, reachable via tunnel, run internal services that don&#8217;t need public access.<\/p>\n<p>The tier-2 laptops win on cost for anything that would otherwise require upsizing your VPS or adding cloud services. A \u20b92000\/month managed log ingestion service, replaced by a laptop that was already paid for and electricity that&#8217;s already being consumed. A \u20b9800\/month additional VPS for running n8n automations, replaced by laptop number two. Cloud GPU credits for LLM experiments, replaced by laptop number four running inference locally.<\/p>\n<p>The pattern that makes this work:<\/p>\n<ol>\n<li>WireGuard on every machine, Linode as hub<\/li>\n<li>Docker + Docker Compose on each laptop<\/li>\n<li>Everything defined in a <code>docker-compose.yml<\/code> in git<\/li>\n<li>State backed up to Linode or object storage<\/li>\n<li>If a laptop dies, <code>docker compose up<\/code> on another one<\/li>\n<\/ol>\n<p>The laptops become disposable. The services become portable. The cost stays zero.<\/p>\n<p>This is what I&#8217;ve started calling <strong>Vibe Driven Infrastructure<\/strong> \u2014 building your stack iteratively by feel, shipping incrementally, letting real constraints (cost, hardware availability, actual traffic) drive architectural decisions rather than imagined future scale. The same philosophy I apply to product development, applied to the infrastructure underneath it.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"What_Id_Do_Differently\"><\/span>What I&#8217;d Do Differently<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p><strong>Start with the tunnel.<\/strong> I spent time evaluating Cloudflare Tunnel and Tailscale before landing on plain WireGuard. For a solopreneur with a VPS that has a public IP, WireGuard hub-and-spoke is the simplest, cheapest, and most private option. No third party in the data path. No commercial-use licensing questions. Just keys and configs.<\/p>\n<p><strong>Ship OpenObserve natively first, Docker second.<\/strong> OpenObserve provides a single <code>.exe<\/code> for Windows that runs without Docker at all. For a dedicated single-purpose machine, the native binary + NSSM service wrapper is simpler than the Docker path. I went Docker because I wanted the flexibility for future services on this machine \u2014 which turned out to be the right call \u2014 but if you just want OpenObserve running in ten minutes, skip Docker entirely.<\/p>\n<p><strong>Fix the Vector config before starting it.<\/strong> Vector ships with a demo config that generates fake syslog data. The first time I started it, I watched beautiful fake logs scroll by for thirty seconds before realising none of it was from my nginx. Always <code>cat<\/code> the config before starting a service.<\/p>\n<p><strong>Budget for the last 10%.<\/strong> Getting logs to <em>arrive<\/em> took an afternoon. Making the pipeline survive reboots, disconnects, and a closed lid \u2014 structured parsing, a systemd service, three separate layers of Windows auto-start \u2014 took longer than the entire happy path before it. The demo is the easy part. Plan for the hardening, because that&#8217;s the part that decides whether you have a server or a science project.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"The_Current_State_and_Whats_Next\"><\/span>The Current State and What&#8217;s Next<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The pipeline is live and, more to the point, durable. nginx logs are parsed into structured fields, shipped over the tunnel, stored in OpenObserve, capped at 45 days so the SSD never fills, and queryable from a browser. Vector runs as a systemd service on the Linode; the laptop recovers from a full reboot with nobody in the loop. I&#8217;ve stopped thinking about it \u2014 which is the only honest measure of &#8220;done&#8221; for infrastructure.<\/p>\n<p>Beyond that, the roadmap for the home lab:<\/p>\n<ul>\n<li><strong>Laptop 2:<\/strong> n8n for workflow automation (Quizwrap notifications, Sage platform webhooks)<\/li>\n<li><strong>Laptop 3:<\/strong> Staging environment for Quizwrap and Sage<\/li>\n<li><strong>Laptop 4:<\/strong> LLM\/RAG experiments and the BigQuery dashboard backend<\/li>\n<li><strong>Laptop 5:<\/strong> Cold spare<\/li>\n<\/ul>\n<p>All of it behind the same WireGuard hub. All of it accessible via SSH port forwards. All of it running on hardware that was otherwise doing nothing.<\/p>\n<p>The thrown-away laptop isn&#8217;t thrown away anymore. And neither are the other four.<\/p>\n<hr \/>\n<h2><span class=\"ez-toc-section\" id=\"The_Stack_For_Reference\"><\/span>The Stack, For Reference<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<table>\n<thead>\n<tr>\n<th>Component<\/th>\n<th>Tool<\/th>\n<th>Where It Runs<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Log shipper<\/td>\n<td>Vector 0.56.0 (systemd service, nginx parsing)<\/td>\n<td>Linode<\/td>\n<\/tr>\n<tr>\n<td>Log store + UI<\/td>\n<td>OpenObserve (Docker, 45-day retention)<\/td>\n<td>Home laptop<\/td>\n<\/tr>\n<tr>\n<td>Encrypted tunnel<\/td>\n<td>WireGuard<\/td>\n<td>Linode hub + laptop peer<\/td>\n<\/tr>\n<tr>\n<td>Remote desktop<\/td>\n<td>Windows App (Microsoft)<\/td>\n<td>Mac \u2192 Linode \u2192 Laptop<\/td>\n<\/tr>\n<tr>\n<td>Container runtime<\/td>\n<td>Docker Desktop 4.77.0<\/td>\n<td>Home laptop<\/td>\n<\/tr>\n<tr>\n<td>Server OS<\/td>\n<td>Ubuntu 24.04<\/td>\n<td>Linode<\/td>\n<\/tr>\n<tr>\n<td>Laptop OS<\/td>\n<td>Windows 10 Pro<\/td>\n<td>Home laptop<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Total monthly cost added to my infrastructure bill: <strong>\u20b90<\/strong>.<\/p>\n<hr \/>\n<p><em>If you&#8217;re a solopreneur running lean, I&#8217;d love to hear what spare compute you have lying around and what you&#8217;re doing with it. Find me on LinkedIn or drop a comment below.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A solopreneur&#8217;s story of \u20b90 infrastructure, WireGuard tunnels, and the laptop nobody wanted. There&#8217;s a 2014 HP Notebook sitting on my desk right now. Intel i5-4210U, 16GB RAM, 238GB SSD. Until this morning it was doing absolutely nothing \u2014 the kind of machine that&#8217;s too old to be your daily driver but too functional to [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,30],"tags":[],"class_list":["post-816","post","type-post","status-publish","format-standard","hentry","category-entrepreneurship","category-thoughts"],"_links":{"self":[{"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=\/wp\/v2\/posts\/816","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=816"}],"version-history":[{"count":2,"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=\/wp\/v2\/posts\/816\/revisions"}],"predecessor-version":[{"id":818,"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=\/wp\/v2\/posts\/816\/revisions\/818"}],"wp:attachment":[{"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=816"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=816"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cyberaka.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=816"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}