This walkthrough takes a freshly provisioned Ubuntu 24.04 instance from “root password in an email” to a hardened web server answering HTTPS in about 25 minutes. Every command is copy-paste ready, and the timing table at the end is based on a 2 vCPU / 2 GB instance on local NVMe. If you have not picked a machine yet, the VPS plans with full root access include the specs this guide assumes: full root, KVM virtualization, and a dedicated IPv4.
What a fresh instance gives you (and what it does not)
- An IPv4 address and a root login (password or an injected key).
- No firewall, no swap on small tiers, no web server, no monitoring.
- A default sshd that still accepts password authentication for root.
Step 1 – Create a sudo user (2 min)
ssh [email protected]
adduser deploy --gecos ""
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy/ # only if you logged in with a key
su - deploy
Step 2 – Install your key and shut the password door (4 min)
From your laptop, push the public key instead of editing files by hand:
ssh-keygen -t ed25519 -C "deploy@prod-01"
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
ssh [email protected] "sudo sshd -T | egrep 'permitrootlogin|passwordauthentication'"
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
sudo sshd -t && sudo systemctl reload ssh
# verify BEFORE closing this session
ssh -o PreferredAuthentications=password [email protected] # must fail
Step 3 – Firewall and brute-force protection (3 min)
sudo apt update && sudo apt -y install ufw fail2ban
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw --force enable
sudo ufw status verbose
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Step 4 – Swap and kernel defaults for a 2 GB box (2 min)
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo sysctl -w vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-vps.conf
Step 5 – Nginx and a landing page (4 min)
sudo apt -y install nginx
sudo tee /var/www/html/index.html <<'EOF'
<h1>prod-01 is live</h1>
EOF
sudo nginx -t && sudo systemctl enable --now nginx
curl -sI http://203.0.113.10 | head -1
Step 6 – Free TLS with Certbot (6 min)
sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com --redirect --agree-tos -m [email protected] --no-eff-email
sudo systemctl list-timers | grep certbot
curl -sI https://example.com | head -3
Step 7 – One-pass verification
ss -tlnp | awk 'NR==1 || /LISTEN/'
sudo ufw status | head -8
free -m | head -2
ls -l /etc/letsencrypt/live/example.com/fullchain.pem
| Step | Wall time | Typical mistake |
|---|---|---|
| sudo user | 2 min | Skipping the rsync of .ssh |
| SSH lockdown | 4 min | Closing the session before testing |
| UFW + fail2ban | 3 min | Enabling UFW without the OpenSSH rule |
| Swap | 2 min | 800 MB swap on a 2 GB instance |
| Nginx | 4 min | Forgetting nginx -t before reload |
| Certbot | 6 min | DNS A record not propagated yet |
| Verify | 2 min | Trusting curl instead of ss -tlnp |
Troubleshooting the first hour
| Error | Cause | Fix |
|---|---|---|
| ssh: Permission denied (publickey) | Key was copied into /root only | rsync ~/.ssh before switching users |
| Locked out after enabling UFW | OpenSSH rule was never added | Use the provider console and run ufw allow OpenSSH |
| Default Nginx page still shown | DNS or server_name mismatch | curl -H “Host: example.com” http://127.0.0.1 |
| certbot: DNS problem | A record missing or still propagating | Check with dig +short, then use –dry-run |
| Site slow right after go-live | No swap, PHP pool sized for a bigger box | Revisit step 4 and size pm.max_children from free -m |
Two habits prevent most of these: keep a second SSH session open before you touch sshd or UFW, and verify DNS from outside your own network with dig +short example.com before running Certbot. Once HTTPS answers, add automated security updates and a one-minute external uptime check against the public URL, so the next failure is caught by monitoring rather than by a customer.
Step 8 – Automate patches and fix the clock
The setup is not finished until the box patches itself. Kernel and library updates land weekly; done by hand they effectively never happen, and a stale OpenSSL is a far bigger risk than an unpatched Nginx config.
sudo apt -y install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
sudo systemctl enable --now unattended-upgrades
sudo timedatectl set-timezone UTC
sudo apt -y install chrony && chronyc tracking | head -3
tail -5 /var/log/unattended-upgrades/unattended-upgrades.log
Leave reboots for the maintenance window rather than disabling them: Unattended-Upgrade::Automatic-Reboot "true" with Automatic-Reboot-Time "04:00" keeps kernel patches from piling up unapplied, at the cost of one restart a month. If the instance runs a database, add a pre-reboot script that checks replication lag or drains the application first. Steps 1 to 7 take about 25 minutes on a good connection; step 8 adds three more. The result is patched, firewalled, HTTPS-only, brute-force protected and time-synced – everything after that belongs to your application.
Next: deploy your application, then lock in monitoring and automated updates. The VPS plans with full root access page lists which tiers include snapshots and dedicated vCPU, which decides whether you need step 4 at all.



Leave a Reply
You must be logged in to post a comment.