Server Troubleshooting Guide

Symptom-first response for skyhouse.dev

Find the row that matches your alert text or what you're seeing, jump to that card, and work top to bottom: rule out the obviousdiagnosefix. Most cards also flag when a symptom could mean a break-in rather than a hiccup — when in doubt, run the 2-minute triage in §1 first. Companion docs: the Cheat Sheet (ports, paths, service map) and /home/plex/docs/server-context.md (full orientation). Baseline captured 2026-05-20.

1. Is this a break-in? — 2-minute triage

Run this whenever an alert has no obvious, self-inflicted cause — especially CPU, memory, or disk surprises. In March 2026 this server ran a cryptominer for 19 days; the full worked example is in /home/plex/incident-report-2026-03-03.md.

CHECK 1 — processes

Look at the biggest CPU and memory consumers. You should recognize every one.

top            # press 'q' to quit; live %CPU
ps aux --sort=-%cpu | head -15
ps aux --sort=-%mem | head -15

Normal on this box: containerd, dockerd, the 15 containers' processes, Plex Media Server / Plex Transcoder, immich, netdata; and from the desktop session Xorg, gnome-shell, chrome, plus the CLI tools claude, agy, zed. A random-named binary burning CPU — especially owned by a container user — is the classic miner signature. Note: ps %CPU is a lifetime average, not "right now"; trust top for instantaneous load.

CHECK 2 — hidden binaries & connections

Malware often deletes its own binary and dials out to a pool/C2.

# processes running from a deleted or /tmp binary:
ls -la /proc/*/exe 2>/dev/null | grep -E 'deleted|/tmp/'
# established outbound connections (sudo adds process names):
sudo ss -tnp state established

Normal outbound: Plex's pub-sub server, update checks to Google / Cloudflare / npm / plex.tv, and LAN devices (e.g. a Chromecast on 192.168.1.x). A container talking to an unfamiliar IP on an odd high port is worth chasing.

CHECK 3 — footholds
sudo ss -tlnp                       # listening ports + owning process
last -n 20                          # recent logins
sudo lastb | head -20               # recent FAILED logins
crontab -l                          # plex user cron
sudo ls -la /etc/cron.d /etc/cron.daily
cat ~/.ssh/authorized_keys          # every key should be yours
sudo fail2ban-client status sshd    # current SSH bans

Cross-check listening ports against the Cheat Sheet's port table. An unexpected listener, an unfamiliar SSH key, or a cron entry you didn't add are all red flags.

VERDICT

If something is in a container (docker top <name> shows the rogue process), the blast radius is usually that container — stop it (docker stop <name>) and rebuild from clean source. If a rogue process is on the host, or you find a host cron / SSH key you didn't create, treat it as a host compromise: isolate (pull the network), preserve evidence, and rebuild. When unsure, stop the suspect container — that is reversible and buys time.

2. "The primary drive is unexpectedly full"

Netdata texts about low space on /. The OS drive is a 120 GB SSD (/dev/sda3) — small, and it fills.

RULE OUT

Did you just copy something large into /home/plex? Is a big Plex transcode or a media-processing job running right now? Those are self-inflicted, not failures.

DIAGNOSE
df -h /
sudo du -xhd1 /     | sort -h | tail -12   # biggest dirs on the OS disk
sudo du -xhd1 /var  | sort -h | tail -12
docker system df                          # image / build-cache bloat
journalctl --disk-usage

On this server the usual culprits, in order: Docker (/var/lib/docker is on the OS disk — ~21 GB of images plus a multi-GB build cache), /var/log, the systemd journal, old kernels (3 are installed), and the Plex transcoder's temp files if they land on /.

FIX
docker image prune              # delete unused images
docker builder prune            # delete build cache (safe; rebuilds slower once)
sudo journalctl --vacuum-size=200M
sudo apt clean                  # clear downloaded .deb cache
sudo apt autoremove --purge     # remove old kernels & orphan packages

Avoid bare docker system prune -a — it also deletes images not tied to a running container, forcing big re-pulls. The durable fix is the larger primary disk: see /home/plex/docs/disk-cutover-guide.md.

BREACH CHECK

If the growth has no explanation — not Docker, not logs, not media — a dropped payload or attack logging is possible. Run §1.

3. "Temp / scratch space is dangerously low"

Media-processing jobs (Plex transcoding, Handbrake, the Google-Takeout scripts) write large temp files. If /tmp or the transcode dir fills, jobs fail mid-run.

RULE OUT

A job legitimately in progress will use scratch space — that is expected. The concern is space that stays consumed with nothing running.

DIAGNOSE
df -h / /tmp
du -sh /tmp/* 2>/dev/null | sort -h | tail
ls -lt /tmp

Plex transcodes to its Transcoder temporary directory (Plex settings → Transcoder, or TranscoderTempDirectory in Preferences.xml); if that points at /, a 4K transcode can eat many GB. A crashed job can also leave orphaned temp files behind.

FIX

Delete orphaned temp files that no live process owns. Finish or stop a stuck job. Longer term: point Plex's transcoder temp at a media drive with room (one of the multi-TB /media/plex* mounts), and lean on the planned bigger OS disk — expanding scratch space is the whole reason for the swap.

4. "Ongoing CPU usage warnings"

Repeated texts from cpu_watchdog.sh — it fires when a process holds >50% CPU for 10+ minutes and is not on its allowlist (Plex, immich, handbrake and system tasks are already allowlisted).

RULE OUT

Did you start something heavy that simply isn't on the watchdog's allowlist — a manual ffmpeg, a big rsync, an immich library re-scan? If so it is benign; the fix is to add it to ALLOWLIST in /usr/local/bin/cpu_watchdog.sh.

DIAGNOSE
top                                   # instantaneous; find the PID
ps -eo pid,ppid,user,%cpu,etime,comm --sort=-%cpu | head
ps -ef --forest                        # who is the parent?
docker stats --no-stream               # per-container CPU

Identify the process, its user, and whether it lives in a container. The March cryptominer showed up exactly here: sustained high CPU by a process the watchdog did not recognize, running inside music-discovery-client.

BREACH CHECK

If you cannot account for the process — unfamiliar name, odd user, running from a container — go straight to §1. A cryptominer is the single most likely cause of an unexplained sustained-CPU alert on a public-facing server.

5. "Memory / swap pressure"

Netdata warns on RAM or swap. This was the first symptom of the March incident (swap hit 98%). The box now has 62 GB RAM and ~35 GB swap, so genuine pressure is less common — but not impossible.

DIAGNOSE
free -h
ps aux --sort=-%mem | head -12
docker stats --no-stream
FIX / RULE OUT

Benign: the desktop Chrome browser routinely holds several GB; many simultaneous Plex transcodes and immich machine-learning runs are memory-hungry by design. Closing browser tabs or letting jobs finish resolves it.

BREACH CHECK

An unrecognized process high in the memory list — run §1.

6. Sites work inside the LAN but not from outside

You can reach plex.skyhouse.dev etc. on the home network, but they're dead from the internet (test from your phone on cellular). The internet→router→server path or DNS is broken. /etc/hosts has local overrides for some subdomains, which is exactly why "inside" can keep working while "outside" is down.

DIAGNOSE — work the chain in order
# 1. Public IP vs what DNS says (the recurring failure mode):
curl -s ifconfig.me ; echo
dig +short skyhouse.dev @1.1.1.1
# 2. Is the reverse proxy up? (80/443 reach nothing if not)
docker ps | grep npm-app-1

If the public IP and the DNS A-record disagree, DNS is stale — the once-every-few-months event. The DDNS pipeline (ip-watch.sh) should fix it; if it hasn't, correct the record at Dynadot manually. The router is also reachable directly at the always-current hostname cw526dc.glddns.com.

FIX — other links in the chain

If DNS is correct but it's still dead from outside: confirm the router port-forwards (WAN 80, 443, 32400, 2222 → 192.168.1.136) in the GL.iNet admin; confirm the modem is still in bridge mode (a modem reset can re-enable its router and double-NAT you, killing all inbound); and confirm the server still holds its DHCP-reserved 192.168.1.136.

BREACH CHECK

Low risk here — this is almost always plumbing. But if DNS records were changed and you didn't change them, check the Dynadot account for unauthorized access.

7. A standard code deploy can't connect

An ssh / scp / rsync / git deploy to the server times out or is refused.

RULE OUT — the port

SSH is on 2222, not 22. A deploy tool defaulting to 22 will silently fail. ssh -p 2222 plex@skyhouse.dev.

DIAGNOSE
# Did fail2ban ban your deploy host? (a few bad auths = 24h ban)
sudo fail2ban-client status sshd
# Is password auth currently off? (lock_ssh.sh sets this)
sudo sshd -T | grep -i passwordauthentication

The most common non-obvious cause: a deploy script retried with a wrong key/password, tripped fail2ban (3 strikes), and got the source IP banned for 24h. The second: lock_ssh.sh was run, so password auth is off and a password-based deploy now needs a key.

FIX
sudo fail2ban-client set sshd unbanip <YOUR-IP>
/home/plex/unlock_ssh.sh        # re-enable password auth, if needed

Also rule out: public IP changed (the deploy targets the hostname → §6), or sshd itself is down (systemctl is-active ssh from the console).

8. SSH won't connect at all

No SSH access even with the right port and credentials.

WORK THROUGH

Port 2222? · fail2ban ban on your current IP (test from a different network, or check from the console)? · password auth turned off by lock_ssh.sh and you have no key enrolled? · sshd stopped? · the whole server down (§10)?

RECOVERY — the console is the safety net

This server has a physical desktop (keyboard + monitor). From that local console you can always log in and run /home/plex/unlock_ssh.sh, sudo fail2ban-client set sshd unbanip <IP>, or sudo systemctl restart ssh — no network required. You cannot lock yourself out permanently.

9. One site / service is down

Uptime Kuma alerts, or you notice a single subdomain failing while the rest are fine.

IDENTIFY & DIAGNOSE

Map the failing site to its backend:

SiteBackendPort
plex.skyhouse.devplexmediaserver (systemd)32400
photos.skyhouse.devimmich_server2283
music.skyhouse.devmusic-discovery-client / -server8000 / 8001
vault.skyhouse.devvaultwarden (LAN-only)80 (internal)
share.skyhouse.devpairdrop3000
btst.skyhouse.devsharedmoments5001
monitor.skyhouse.devnetdata (systemd)19999
status.skyhouse.devuptime-kuma3001
# containerized service:
docker ps -a | grep <name>             # Up, or Exited?
docker logs --tail 50 <name>
# systemd service (Plex, netdata):
systemctl status <unit>
FIX
docker start <name>                            # or, from its compose dir:
cd /home/plex/<dir> && docker compose up -d
sudo systemctl restart <unit>                  # for Plex / netdata

If it crashes again immediately: read the logs, check disk space (§2). If the affected service is music-discovery-client, recall its incident history — run §1 before assuming it's a benign crash.

10. Nothing is reachable — whole server down

Every service is unreachable, inside and out.

ESCALATE

From a LAN device: does ping 192.168.1.136 answer? If no — the box is off, crashed, or off the network: check power, the network cable, and the router. If ping answers but services don't — treat as a stack problem (Docker daemon down? → check from the console). Check uptime for an unexpected reboot.

AFTER A REBOOT

Most services auto-start (Docker restart: unless-stopped, systemd units). Once it's back, run the verification checklist on the Cheat Sheet to confirm every service and mount came back. Note: the media drives mount with nofail, so the system boots even if a drive is missing — verify mounts explicitly.

11. Alerts that need no action

Not every text is a problem. These are routine:

AlertMeaning & response
"Plex update available"From plex-check. Run sudo plex-update when convenient.
"Next.js update available"From nextjs-check. Schedule a music-discovery-client rebuild — not urgent unless it's a security release.
"Public IP changed"From ip-watch.sh. Informational — DNS updates itself.
fail2ban ban noticesRoutine internet background noise. Only notable if the banned IP is yours (→ §7).
← Back to Admin Hub