Home

Tuning a tiny Linux home server: notes that survived a year

· 2 min read·Linux

The handful of sysctl, systemd and filesystem tweaks that actually mattered on a low-power home server, and the ones I reverted.

A year ago I replaced a noisy old desktop with a fanless mini PC as the home server. It runs a few containers, backups and the reverse proxy for everything else. These are the tweaks that are still in place twelve months later, and the ones that quietly disappeared.

Power budget

The server idles at about 6 W. That number is the whole reason the old desktop had to go, so it is worth being explicit about what adds up to it.

Pidle=Pboard+∑i=1nPdisk,i+PnicP_{idle} = P_{board} + \sum_{i=1}^{n} P_{disk,i} + P_{nic}
ComponentIdleUnder load
Board and CPU4 W18 W
NVMe system disk1 W3 W
2.5 GbE NIC1 W2 W

Kernel settings that stayed

Most of the sysctl folklore made no measurable difference. Two settings did.

# /etc/sysctl.d/90-homeserver.conf
vm.swappiness = 10
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
  • Lower swappiness stopped the box from paging out the containers overnight when the backup job filled the page cache.
  • BBR with fq made the off-site backup upload roughly twice as fast over the asymmetric home connection.

systemd units instead of cron

Every periodic job is a service plus a timer. The unit below runs the backup and gets killed if it hangs, which cron never did for me.

[Unit]
Description=Nightly restic backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
TimeoutStartSec=2h
Nice=10
IOSchedulingClass=idle

The matching timer uses Persistent=true so a missed run happens at the next boot instead of being skipped.

A tiny watchdog

The only monitoring is a script that posts a message when a service has been failed for more than a few minutes. It is short enough to show in full.

import subprocess
import sys

failed = subprocess.run(
    ["systemctl", "--failed", "--no-legend", "--plain"],
    capture_output=True,
    text=True,
    check=False,
).stdout.split()

units = [line for line in failed if line.endswith(".service")]
if units:
    print("failed units:", ", ".join(units))
    sys.exit(1)

The best monitoring is the kind you will not disable after the third false alarm.

What I reverted

  1. Custom I/O schedulers. The default mq-deadline on NVMe was already fine, and the change made one of the disks noticeably slower.
  2. Transparent huge pages set to always. Memory usage went up, nothing got faster.
  3. A hand-tuned ZFS ARC limit. Reverting to the default stopped a weekly slowdown that I had been blaming on the containers.

If there is one takeaway: measure before and after, write the number down next to the change, and revert anything that did not move it.

Comments 0

No comments yet.

Sign in with GitHub to join the discussion.

Only your public profile is used.