Blog
Homelab Kickoff: Building My First Linux Server from an Old PC
That 2018 mini PC of mine — an i5-8500, 16 GB of RAM, a 512 GB SSD plus a 2 TB spinning disk — had been gathering dust in the corner for two years. The dust was more visible than any benchmark. I didn't want to turn it into a "can-run-everything" wonder machine overnight. I just wanted a base that stays on all the time, that I can reach remotely, and that I know how to rebuild when it breaks. NAS, reverse proxy, game servers — everything later would have to stand on top of it.
Cloud VPSes are fine for public-facing work; this box at home is better for big storage and real network experiments. The cost of trial and error is mostly the electric bill and lost sleep, not a surprise invoice. So here's the real seven days, not a best-practices checklist.
February 7: Do the Math Before You Crack the Case
For an always-on machine, power draw matters more than benchmarks. I measured it with a power meter: about 28 W idle, roughly 35–45 W steady-state once Docker was installed. At 40 W that's about one kilowatt-hour a day — a dozen-odd yuan a month. Acceptable. The old desktop in the same room idles at 90 W and up; over the long term, something like an N100 is the smarter pick.
Networking, I decided, means wired. Wireless is fine as a backup; as your primary link you'll regret it the night NFS hangs. 8 GB of RAM gets you started, but once Docker containers pile up I'd go 16 GB. SSD for the system disk; a spinning disk is fine for data. And turn on "power on after AC loss" in the BIOS — one less trip to the living room after an outage.
I stuck a sticky note on the side of the case: lab-core, Ubuntu, data on /data, static IP 192.168.1.10. Future me, three months down the line, thanks this dumb little habit.
February 8: Install the OS — a Base You Can Reach Remotely, Nothing More
Ubuntu Server 24.04 LTS, chosen for the documentation and because it plays nice with Docker and 1Panel. Debian is more conservative; rolling distros are more fiddly. For a first box, keep the complexity in your apps, not in your distro.
During install I ticked OpenSSH, named the host lab-core, set the timezone to Asia/Shanghai. The second disk gets formatted, then mounted at /data by UUID in /etc/fstab — don't reference /dev/sdb1, device names drift:
sudo timedatectl set-timezone Asia/Shanghai
lsblk -f
# UUID=xxxx-xxxx /data ext4 defaults,noatime 0 2
Baseline right after install — don't rush straight to Immich:
sudo apt update && sudo apt upgrade -y
sudo apt install -y vim curl git htop tmux tree ufw fail2ban jq ca-certificates
Switch SSH to keys and kill password auth. Generate ed25519 locally, copy the public key over:
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
X11Forwarding no
EOF
sudo sshd -t && sudo systemctl reload ssh
The rule went on line two of the sticky note: keep the old session open, open a new window and confirm you can get in, then close the old one. Firewall: SSH only:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
That evening I installed unattended-upgrades, followed the official docs to install Docker Engine and the Compose plugin, added my user to the docker group, logged back in, and ran hello-world once.
February 9–10: Directories and Networking, Deliberately Slow
I settled on four roots:
/opt/stack— compose projects/data— big data/var/backups/lab— local staging~/notes— change log, into git when possible
DHCP static lease on the router. Added lab-core.lan to the home DNS. System and data are separate, so a reinstall hurts an order of magnitude less.
Those two days I did not install Immich, did not start Minecraft, did not try Kubernetes. I only verified: key-based login survives a reboot, ufw status shows only the rules I expect, docker ps works, fstab uses UUIDs, and the IP plus disk assignments are in the notes.
One night I deliberately pulled the power plug and put it back. Power-on-after-outage worked, and /data mounted. That moment reassured me more than any container ever coming up. A neighbor asked why the lights flickered; I said I was running a power-outage drill for the lab. Sounds serious. Really, I'm just afraid of my own laziness.
The day the directory conventions went into git, I also sketched a crude tree diagram: the system disk holds system, the data disk holds data, backup staging is never primary storage. Later, Immich and game saves both benefited from that boring decision.
February 11: I Almost Locked Myself Out
Mid-morning I got too clever: I disabled password auth before ssh-copy-id had actually succeeded. Then I closed the old session too. The laptop couldn't connect.
Monitor and keyboard, local login, flip PasswordAuthentication back to yes, re-copy the public key, disable passwords again. Forty minutes, most of it hunting for an HDMI cable.
One lesson went into the notes: before touching sshd, keep an already-authenticated session alive; after the change, verify from a new window. A monitor is an emergency channel, not a badge of shame.
February 12: The First Service, Whose Only Job Was to Prove the Loop
I put up a minimal static page behind Caddy, just for practice. The point wasn't to ship a product; it was to walk the whole loop: compose comes up → still there after reboot → where do the logs live.
mkdir -p /opt/stack/hello /data/hello
# in compose, mount ./html into the container, restart: unless-stopped
docker compose up -d
docker compose logs -f --tail=50
At noon I rebooted on purpose. The page came back and the container reported Up. In the afternoon I changed the data so it writes into /data/hello, deleted and recreated the container, and the content survived — finally dodged the anonymous-volume trap.
Yesterday I forgot to set TZ inside the container, so log timestamps were off by eight hours. Fixed today. Small thing — but it matters later for certificate expiry and alert timing.
February 13: Fill in the Notes, Write Down What "Fit for Duty" Means
Added to the notes: IP, MAC, data-disk UUID, Docker version, backup staging path, power-on-after-outage enabled.
What counts as fit for duty? Not how many apps you've installed — it's these things all holding at once:
- After any power loss, the box comes back up and SSH keys still log in.
- The data disk mounts by UUID, not by device-name luck.
- Docker group permissions work, and compose projects have fixed directories.
- Every change has a notes entry: IP, purpose, backup location.
Only with all four checked do I feel OK telling a friend "there's a server at my house." Before that, it's just an old PC with power. The standard is deliberately boring, because boring is repeatable.
The night I tried wireless as the primary link, NFS nearly wedged. After that, the Ethernet cable stays plugged in and wireless is only for laptop roaming. Leave that kind of detail out of the notes and you'll step in it again in three months.
February 14: The Base Goes On Duty. The Fun Comes Later
Seven days done, sticky note still on the case. The service list is still short: the hello static page, Docker, ufw, key-based SSH.
What it means is order: reachable, updatable, isolatable, expandable. With a base in place, WireGuard and a storage pool stop being scary conversations. A first lab host doesn't need to be flashy; it needs to survive the next reboot.
It's Valentine's Day. A friend asked what I was up to. I said I was celebrating the old PC's first day on the job. They don't get it. Doesn't matter — I do. The seven-day diary closes here; the real experiments start next week, with the first service that cries.