The 30-minute hardening checklist
A fresh Ubuntu VPS from DigitalOcean, Vultr, or Hostinger is exposed to the internet the moment it boots. This checklist closes the standard attack surface in about 30 minutes.
1. Create a sudo user
adduser deploy
usermod -aG sudo deploy
su - deploy
mkdir -p ~/.ssh
echo "PASTE-YOUR-PUBLIC-KEY" >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
2. Lock down SSH
sudo nano /etc/ssh/sshd_config
Set:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Then restart: sudo systemctl restart sshd. Test from a second terminal before closing the first — a typo here locks you out.
3. UFW firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Only add ports your app genuinely needs (e.g. 3000 for Next.js behind a proxy — but better to bind to localhost).
4. fail2ban
sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo systemctl restart fail2ban
sshd jail is enabled by default; add nginx-http-auth or postfix jails for your stack.
5. Auto security updates
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Choose Yes for automatic security updates. Point the mail option at your inbox so you get a summary.
6. Docker isolation
- Add the user to the
dockergroup instead of usingsudo docker:sudo usermod -aG docker deploy. - Run each service in its own container with
--restart=unless-stopped. - Never expose a database port publicly — bind it to
127.0.0.1.
Verification
sudo ufw status verbose
sudo fail2ban-client status sshd
sudo unattended-upgrades --dry-run --debug
For the platform tradeoffs behind choosing a VPS in the first place, see DigitalOcean vs Vultr or deploy a real app with our Next.js on VPS guide.