I run hobby services on a small VM. One morning my Minecraft server was lagging and the CPU was pegged at 100%. A quick look showed several unfamiliar processes hammering all cores. The root cause was a remote code execution in an exposed Next.js app, exploited within days of a public disclosure. Attackers dropped a script, ran a crypto miner and left a few persistence hooks. I removed the files, patched Next.js and hardened the host. The cleanup taught me what I should have automated.
Remote code execution in web frameworks gets scanned and exploited almost instantly. For self-hosted applications that means public-facing ports are invitations. With an RCE the attacker can write files, spawn processes and install a miner that silently consumes CPU. That kills performance and shortens hardware life. It also hides deeper backdoors in cron jobs, systemd timers and forgotten services. For me the obvious signs were high CPU, unknown processes and lag in unrelated services. After containment I checked crontab -l, systemctl list-timers, and the usual places in /etc and /var for dropped scripts. If you see a pattern like that, treat it as a breach until proven otherwise.
Practical hardening cut my exposure. I started with automated updates. On the OS I run unattended-upgrades on Debian/Ubuntu and configure package pinning where required. For Node dependencies I use automated dependency updates from Renovate configured to open small, fast PRs for next and other critical packages. Make sure your CI runs npm audit or equivalent and blocks merges for high-severity alerts. For Next.js security, update the next package to the latest patch as soon as an advisory appears. If your portfolio can be static, serve the static build from a CDN or static host and remove the server component altogether. That removes a lot of attack surface.
I tightened file uploads and execution paths. Any route that accepts files must validate MIME type, check magic bytes, and write uploads outside the web root with safe permissions. Do not let uploaded files keep executable bits. Avoid spawning child_process from user input. If an app needs to run user code, sandbox it in a container with strict seccomp and cgroups limits. Run periodic malware scans with tools like ClamAV and rootkit hunters, and add simple process checks. A small cron that logs ps aux –sort=-%cpu and reports spikes to your monitoring will catch miners quickly.
Monitoring and limits saved my setup from repeat incidents. Set up alerting for sustained high CPU, unusual outbound connections and rapid file changes. Use basic firewall rules and reverse proxies so only intended ports are public. Use SSH on a non-standard port if you like, but more useful is key-only SSH, fail2ban and a restrictive UFW policy. Containerise risky hobby apps and run them under resource limits so one compromised app cannot choke the host. For vulnerability management, subscribe to Next.js release notes, use automated dependency PRs, and schedule at least weekly checks of critical services. If a patch is tagged as RCE or critical, prioritise it and push it through your staggered rollout.
Concrete takeaways from my incident: make OS and package updates automatic, automate dependency updates for next and node modules, lock down file uploads and execution, monitor CPU and processes and audit cron and systemd after any anomaly. Reduce public exposure by serving static content where possible or by placing apps behind a proxy and firewall. These steps cut the chance of a quick compromise and make recovery far less painful.