The Security Reality of Self-Hosting
When you self-host, there is no vendor patching things for you at 2 AM. There is no WAF absorbing probes. The good news: most real-world compromises exploit basic hygiene failures, not novel zero-days. Fixing the basics puts you ahead of 90% of exposed services.
Here are seven actionable steps, ordered by impact-to-effort ratio.
Step 1: Never Expose Services Directly — Use a Web Gateway
Put every service behind a single Caddy or Nginx web gateway. The gateway handles:
- TLS termination (auto-certificates via ACME)
- HTTP security headers (HSTS, CSP, X-Frame-Options)
- Rate limiting
- Access control by source IP or authentication
Direct exposure of application ports (5432, 8080, 3000) to the internet is the single most common mistake self-hosters make.
# Caddy snippet — add to every internal service
app.example.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
forward_to localhost:8080
}
Step 2: SSH Key Authentication Only
Disable password-based SSH immediately after your first key login.
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
Use Ed25519 keys — smaller and faster than RSA:
ssh-keygen -t ed25519 -C "homelab-key-2026"
Step 3: Automatic Security Updates
Enable unattended-upgrades on Debian/Ubuntu to auto-apply security patches:
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
For container images, use Watchtower in monitor-only mode: it alerts you when upstream images have updates without auto-pulling (avoiding surprise breaking changes).
Step 4: Principle of Least Privilege for Containers
Never run application containers as root. Most official images support a non-root user:
services:
app:
image: myapp:latest
user: "1000:1000" # map to a non-root UID
read_only: true # make the container filesystem read-only
tmpfs:
- /tmp # writable temp space only
cap_drop:
- ALL # drop all Linux capabilities
cap_add:
- NET_BIND_SERVICE # add back only what you need
Step 5: Network Isolation with Docker Networks
By default, all Docker containers share a bridge network and can reach each other. Segment them:
networks:
frontend: # web gateway <-> web apps
backend: # web apps <-> databases only
monitoring: # prometheus + exporters
services: db: networks: - backend # db is NOT reachable from frontend web: networks: - frontend - backend
Step 6: Audit Open Ports Monthly
# What's listening?
ss -tlnp
# Cross-check with firewall rules ufw status verbose
# Scan yourself from outside (use a VPS or friend's machine) nmap -sV -p 1-65535 your.public.ip
Remove anything you don't recognize. If a service doesn't need to be reachable from the internet, add a ufw deny rule.
Step 7: Encrypted, Off-Site Backups
A compromised or failed server should cost you data only from the last backup window, not permanently.
# restic backup to Backblaze B2 (encrypted client-side)
restic -r b2:mybucket:homelab init
restic -r b2:mybucket:homelab backup /var/lib/docker/volumes /home
restic -r b2:mybucket:homelab snapshots # verify
Schedule this daily via cron or a systemd timer. Test restoration quarterly — untested backups are just hopeful archives.
Summary Checklist
Self-hosting security is not about exotic tools — it is about consistent application of fundamentals. The WEDC member library provides Ansible roles that automate steps 1–5 across fleets of machines with a single command.