First VPS Setup in 25 Minutes: SSH Keys, UFW, Nginx, and Free HTTPS with Copy-Paste Commands

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
StepWall timeTypical mistake
sudo user2 minSkipping the rsync of .ssh
SSH lockdown4 minClosing the session before testing
UFW + fail2ban3 minEnabling UFW without the OpenSSH rule
Swap2 min800 MB swap on a 2 GB instance
Nginx4 minForgetting nginx -t before reload
Certbot6 minDNS A record not propagated yet
Verify2 minTrusting curl instead of ss -tlnp

Troubleshooting the first hour

ErrorCauseFix
ssh: Permission denied (publickey)Key was copied into /root onlyrsync ~/.ssh before switching users
Locked out after enabling UFWOpenSSH rule was never addedUse the provider console and run ufw allow OpenSSH
Default Nginx page still shownDNS or server_name mismatchcurl -H “Host: example.com” http://127.0.0.1
certbot: DNS problemA record missing or still propagatingCheck with dig +short, then use –dry-run
Site slow right after go-liveNo swap, PHP pool sized for a bigger boxRevisit 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