Turning an Old Laptop into a Production Server — The Hard Way
How I went from a broken laptop with a dead WiFi chip to a self-healing Ubuntu server running live traffic for Changing Room.
The Hardware
I had an old HP laptop sitting around doing nothing.
The plan: turn it into a production server for Changing Room — a virtual try-on app for Indian fashion that lets you see how clothes look on you before buying.
First problem hit me before I even started: the internal WiFi chip was completely dead. Not disabled, not driver issues, just DEAD. No amount of troubleshooting was going to fix it.
Solution: a cheap USB WiFi adapter. Plugged it in, Ubuntu detected it as wlx00e620053aa7 using the mt7601u driver. This little adapter would later cause me more grief than the dead chip ever could, but I'll get to that.
Wiping Kali, Installing Ubuntu Server
The laptop had Kali Linux on it great for security research, wrong tool for running production services. I wiped it clean and installed Ubuntu Server 26.04 LTS.
No GUI. No desktop environment. Just a terminal and a blinking cursor. Exactly what a server needs.
The WiFi Problem, Phase 1
Ubuntu boots up. I run ip a. The USB adapter shows up, good sign. But no IP address, no connection. I try to install packages. Nothing works because there's no internet.
Classic chicken-and-egg: need WiFi to install the tools to configure WiFi.
Solution: USB tethering through a phone.
Connected the phone to the laptop via USB cable. The phone shares its mobile data connection as a USB ethernet adapter, the laptop gets internet through the phone's data. Not elegant, but it works.
With internet via the phone I installed what I needed:
apt install wpasupplicant
Then created the WiFi config with the home network credentials:
# /etc/wpa_supplicant/wpa_supplicant-wlx00e620053aa7.conf
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="YourWiFiName"
psk="YourPassword"
}
Enabled wpa_supplicant as a systemd service tied to the adapter:
sudo systemctl enable wpa_supplicant@wlx00e620053aa7
sudo systemctl start wpa_supplicant@wlx00e620053aa7
The adapter connected to the router. I disconnected the phone. The server was on WiFi.
Fixing the IP Address
DHCP gives you a different IP every reboot. Fine for a laptop, disastrous for a server, your SSH config, tunnel settings, and everything else breaks the moment the IP changes.
The right way to fix this is a DHCP reservation on the router, you log into the router admin panel and tell it to always give the same IP to your adapter's MAC address. But the router admin panel wasn't accessible. (I FORGOT THE PASSWORD AND MY ROUTER IS A BIT HIGHER FOR ME TO REACH AND I AM LAZY AF)
So I did it entirely on the server side using systemd-networkd, just tell the server to always use a specific IP regardless of what the router says:
# /etc/systemd/network/10-wifi.network
[Match]
Name=wlx00e620053aa7
[Network]
Address=192.168.31.XXX/24
Gateway=192.168.31.1
DNS=8.8.8.8
[Link]
RequiredForOnline=yes
This assigns the IP statically on the server side. The router might try to hand out a DHCP lease but the server ignores it and keeps its own IP.
There's a catch though: NetworkManager was also running, and it kept fighting with systemd-networkd over control of the adapter. It would grab a DHCP lease from the router and assign a second dynamic IP alongside the static one, two IPs on one interface, unpredictable routing.
Fix: tell NetworkManager to completely ignore that interface:
# /etc/NetworkManager/conf.d/unmanaged.conf
[keyfile]
unmanaged-devices=interface-name:wlx00e620053aa7
One IP. Static. Permanent.
Setting Up the Production Services
With stable networking sorted, I set up the actual application:
PostgreSQL, local database. All user data, payments, try-on jobs stored here. Managed with Prisma ORM.
API server, a Node/Express backend. Handles authentication (OTP via email), payments (Razorpay), try-on job creation, admin endpoints, and everything the frontend and extension talk to.
Worker, a background service that polls for queued try-on jobs, calls the AI to generate the virtual try-on image, size recommendations, and stores results in Cloudflare R2.
Each runs as a systemd service:
[Unit]
Description=Changing Room API
After=network.target postgresql.service
[Service]
WorkingDirectory=/home/shiv/changing-room/apps/api
ExecStart=/usr/bin/node dist/main.js
Restart=always
RestartSec=5
User=shiv
[Install]
WantedBy=multi-user.target
Restart=always means if the process crashes, systemd brings it back within 5 seconds automatically.
Exposing to the Internet, Cloudflare Tunnel
I had a working server on a local network. Now I needed it reachable from the internet at api.changingroom.in without:
Opening ports on the router (required router access I didn't have)
Exposing my home IP address publicly
Setting up dynamic DNS
Paying for a VPS
Cloudflare Tunnel solves all of this.
cloudflared runs on the server and creates an outbound encrypted connection to Cloudflare's edge network. When someone hits api.changingroom.in, Cloudflare routes the request through the tunnel to the laptop and sends the response back. No inbound ports, no exposed IP, completely free.
cloudflared tunnel create changing-room
cloudflared tunnel route dns changing-room api.changingroom.in
Config:
# ~/.cloudflared/config.yml
tunnel: <tunnel-id>
credentials-file: /home/shiv/.cloudflared/<tunnel-id>.json
ingress:
- hostname: api.changingroom.in
service: http://localhost:8080
- service: http_status:404
cloudflared also runs as a systemd service. All three services, API, worker, cloudflared, start on boot and restart on crash.
The Server Keeps Going Down
Everything worked. I shipped features, processed payments, ran try-ons. Then the server started dropping randomly. No pattern, no warning, just gone. APIs down, tunnel disconnected, SSH timing out.
Every time I went to the laptop and ran ip a, the same story:
wlx00e620053aa7: state DOWN
No IP. No connection. The adapter was present but not talking to the router.
Diagnosing: The rfkill Problem
After several outages I dug into the system journal from the previous boot session:
rfkill: WLAN hard blocked
rfkill: Wi-Fi disabled by radio killswitch; enabled by state file
The culprit: systemd-rfkill.
rfkill is Linux's mechanism for enabling and disabling radio devices, WiFi, Bluetooth, mobile data. It has two types of blocks:
Soft block, software controlled, clearable with a command
Hard block, hardware or firmware level, much harder to clear
systemd-rfkill is a service that saves the rfkill state when you shut down and restores it when you boot. If WiFi was blocked when the machine crashed or shut down unexpectedly, it restores that blocked state on the next boot.
So what was happening:
Something caused the WiFi to go into a blocked state mid-session
The machine eventually rebooted (or I rebooted it)
systemd-rfkillread the saved state and restored the blockwpa_supplicanttried to connect but the radio was blockedThe interface came up with an IP but no actual WiFi association
Server appeared unreachable
Fix: mask systemd-rfkill permanently:
sudo systemctl mask systemd-rfkill systemd-rfkill.socket
This creates symlinks pointing to /dev/null. The service can never run again. WiFi always comes up unblocked on boot.
The Laptop Going to Sleep
Another failure mode I discovered: Ubuntu's power management was suspending the laptop after a period of inactivity. Lid close, idle timer, any of these would suspend the machine, drop the tunnel, and leave the server completely unreachable.
Disable all sleep targets permanently:
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
Ignore lid close:
sudo sed -i 's/#HandleLidSwitch=suspend/HandleLidSwitch=ignore/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind
The laptop now runs indefinitely regardless of lid state or idle time. Close the lid, leave it for days, it stays up.
The Watchdog, Automatic Recovery
Even with rfkill fixed and sleep disabled, there's still a risk:
WiFi dropping mid-session due to interference, a router hiccup, or the USB adapter glitching. Without something to detect and recover from this, the server stays down until someone physically intervenes.
I wrote a watchdog script:
#!/bin/bash
IFACE=wlx00e620053aa7
IP=192.168.31.XXX/24
GW=192.168.31.1
while true; do
if ! ping -c 1 -W 3 $GW > /dev/null 2>&1; then
logger "wifi-watchdog: gateway unreachable, recovering $IFACE"
ip link set $IFACE down
sleep 2
for f in /sys/class/rfkill/rfkill*/soft; do echo 0 > $f; done
ip link set $IFACE up
sleep 3
wpa_supplicant -B -i $IFACE \
-c /etc/wpa_supplicant/wpa_supplicant-$IFACE.conf
sleep 5
ip addr add \(IP dev \)IFACE 2>/dev/null || true
ip route add default via \(GW dev \)IFACE 2>/dev/null || true
fi
sleep 30
done
Every 30 seconds it pings the router gateway. If it doesn't respond:
Brings the interface down cleanly
Clears any
rfkillsoft blocksBrings the interface back up
Reconnects
wpa_supplicantto the WiFi networkRe-assigns the static IP and default route
Recovery happens automatically within 60 seconds. Registered as a systemd service:
[Unit]
Description=WiFi Watchdog
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/wifi-watchdog.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable wifi-watchdog
sudo systemctl start wifi-watchdog
The Windows SSH Mystery
One last puzzle worth documenting: after fixing everything on the server, SSH from my Windows machine on the same network still timed out. The API was responding through the tunnel, the server was clearly up, but ssh shiv@192.168.31.XXX gave Connection timed out every time.
Running tracert on Windows revealed the problem:
1 host.docker.internal [192.168.31.***] Destination host unreachable.
Windows was routing traffic destined for the server back to itself. A stale ARP cache entry meant the packet never left the machine. IPv6 SSH worked as a workaround:
ssh shiv@fe80::2e6:20ff:fe05:*****
Fix: reboot Windows. ARP cache cleared, correct MAC address learned from the router, IPv4 SSH worked immediately.
The Final Architecture
Internet
│
Cloudflare Edge
(api.changingroom.in)
│
Cloudflare Tunnel (encrypted, outbound only)
│
Old HP Laptop, Ubuntu Server 26.04
Static IP: 192.168.31.XXX
│
├── changing-room-api (port 8080, systemd)
├── changing-room-worker (background, systemd)
├── cloudflared (tunnel, systemd)
├── wifi-watchdog (recovery, systemd)
└── PostgreSQL (local, systemd)
Guarantees now in place:
WiFi always unblocked on boot,
rfkillmaskedStatic IP always assigned,
networkdonly, NM unmanagedLaptop never sleeps, all sleep targets masked
WiFi auto-recovers if it drops,
watchdogpings every 30sAll services restart on crash,
systemd Restart=alwaysNo open ports, no exposed home IP, Cloudflare Tunnel
What It Cost
Rs. 0.
A sleepless night that f*ed up my schedule.
Lessons Learned
Mask systemd-rfkill on headless WiFi servers. It saves radio kill states and restores them on boot. If your WiFi was ever in a bad state when the machine shut down, it'll restore that bad state. It causes more problems than it solves on a server.
Pick one network manager and stick to it. NetworkManager and systemd-networkd both want to control your interfaces. Put one in charge, tell the other to stay away. The unmanaged devices config in NM is your friend.
If you can't access your router, do static IP on the server side. It works just as reliably, the server simply ignores DHCP and keeps its own configured address.
A watchdog is not optional for WiFi servers. Ethernet is reliable. WiFi is not. Always have something that can recover the connection without human intervention.
Cloudflare Tunnel is the right answer for home servers. No port forwarding, no dynamic DNS, no exposed IP address, free tier is more than enough. It just works.
Old hardware is underrated. 4GB RAM, 500GB drive. The heavy lifting, AI inference, image processing, is done by external APIs. The server just orchestrates and stores. CPU barely breaks a sweat.
Changing Room is a virtual try-on extension for Myntra and Ajio. Coming soon at changingroom.in.
