Most tutorials on how to set up a Linux VPS stop at "log in as root and apt-get install stuff." That's how you end up with a compromised server at 3 a.m. on a Tuesday.
I've provisioned probably 60–70 VPS instances over the last nine years. The first few were disasters — open ports, root login enabled, no backups. I've since built a repeatable 30-minute setup routine that I run on every new machine. This post is that routine, written out step by step.
We'll cover: picking a provider and distro, first login hardening, firewall setup, creating a deploy user, configuring swap, and a quick sanity-check checklist. By the end you'll have a server that's actually ready for production, not just "running."
Picking a Provider and Distro
I run everything on Hetzner. A CAX11 (Ampere ARM, 2 vCPU, 4 GB RAM) costs €3.79/month as of mid-2025. DigitalOcean's equivalent is roughly 3× that price for the same specs. Vultr and Linode sit somewhere in between. For indie hackers bootstrapping on a budget, Hetzner is the obvious call.
For distro, I use Ubuntu 24.04 LTS. Not because it's the coolest, but because:
- LTS means security patches until 2029.
- Stack Overflow answers exist for every edge case.
- Most third-party install scripts test against Ubuntu first.
Debian 12 is a fine alternative if you want something leaner. Avoid anything bleeding-edge (Arch, Fedora) on a production VPS unless you enjoy surprise breakage.
First Login and Immediate Hardening
Your provider will email you a root password or let you inject an SSH key during provisioning. Always inject your SSH key at provisioning time — it saves a step.
SSH in as root:
ssh root@YOUR_SERVER_IP
First thing: update everything.
apt update && apt upgrade -y
This takes 2–5 minutes. Don't skip it. You're closing known CVEs before you do anything else.
Create a Non-Root Deploy User
Never run your apps as root. Create a user:
adduser deploy
usermod -aG sudo deploy
Copy your SSH key to the new user:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Open a second terminal and verify you can log in as deploy before you lock out root. Seriously — open that second terminal now.
ssh deploy@YOUR_SERVER_IP
sudo whoami # should return root
If that works, go back to your root session and harden SSH.
Harden SSH Configuration
Edit /etc/ssh/sshd_config:
nano /etc/ssh/sshd_config
Change or add these lines:
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
Changing the default port from 22 to something like 2222 won't stop a determined attacker, but it cuts out 95% of the automated noise in your auth logs. Worth the minor inconvenience.
Restart SSH — but keep your current session open:
systemctl restart sshd
Test the new config from a third terminal:
ssh -p 2222 deploy@YOUR_SERVER_IP
If it works, you're good. If not, your existing sessions are still live and you can fix the config.
Set Up a Firewall With UFW
UFW (Uncomplicated Firewall) ships with Ubuntu and wraps iptables in something humans can actually read.
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # your new SSH port
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enable
Verify:
ufw status verbose
You should see something like:
Status: active
To Action From
-- ------ ----
2222/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
Don't open ports you don't need. If you're running a database, bind it to 127.0.0.1 — never expose Postgres or MySQL directly to the internet unless you have a very specific reason and know exactly what you're doing.
Configure Swap Space
This one gets skipped constantly. On a 2–4 GB RAM VPS, a memory spike from a deploy or a traffic burst can OOM-kill your app. Swap gives you a buffer.
Check if you already have swap:
swapon --show
If it's empty, create a 2 GB swapfile:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
Make it persistent across reboots:
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Tune swappiness — the default of 60 is too aggressive for a server:
echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -p
A swappiness of 10 means the kernel will only use swap when RAM is 90% full. That's what you want on a VPS.
Install Fail2Ban
Even on a non-standard SSH port, bots will find you within hours. Fail2Ban reads your auth logs and auto-bans IPs after repeated failed logins.
apt install fail2ban -y
Create a local config so your settings survive package updates:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
nano /etc/fail2ban/jail.local
Find the [sshd] section and update it:
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
findtime = 600
Restart and enable:
systemctl restart fail2ban
systemctl enable fail2ban
Check the status after a few hours:
fail2ban-client status sshd
You'll almost certainly see bans already. The internet is relentless.
Set Up Automatic Security Updates
I don't want to think about patching every week. For security-only updates, unattended-upgrades handles it automatically.
apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades
Answer "Yes" to the prompt. This configures the package to auto-install security updates from Ubuntu's security repository only — not every available update, which could break things.
Verify the config:
cat /etc/apt/apt.conf.d/20auto-upgrades
You want to see:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Quick Sanity Check Before You Deploy Anything
Before you start throwing apps at this server, run through this checklist:
| Check | Command | Expected Result |
|---|---|---|
| Root login disabled | grep PermitRootLogin /etc/ssh/sshd_config |
PermitRootLogin no |
| Password auth off | grep PasswordAuthentication /etc/ssh/sshd_config |
PasswordAuthentication no |
| Firewall active | ufw status |
Status: active |
| Swap enabled | swapon --show |
Shows /swapfile |
| Fail2Ban running | systemctl status fail2ban |
active (running) |
| Auto-updates on | systemctl status unattended-upgrades |
active |
If everything checks out, your server is ready. Not perfect — there's always more you can do (AppArmor profiles, auditd, intrusion detection) — but this baseline handles 90% of the threat surface for a typical indie hacker project.
What to Install Next
Once the baseline is done, the next steps depend on your stack. Here's what I typically install in order:
- Nginx or Caddy — I've been using Caddy lately because automatic TLS via Let's Encrypt requires zero config.
apt install caddyand you're done. - Docker (optional) — If you're containerizing apps, follow the official Docker install for Ubuntu. Don't use the version in Ubuntu's default repos; it's usually outdated.
- Your runtime — Node.js via
nvm, Python viapyenv, or whatever your app needs. Pin a specific version. Don't rely on whateveraptserves up.
For a deeper look at running multiple apps on one VPS without them stepping on each other, check out our guide on reverse proxy setup with Nginx — it covers virtual hosts and SSL termination in detail.
If you're thinking about whether to containerize everything or run bare processes, our post on Docker vs bare metal for small projects walks through the tradeoffs honestly.
The Comparison: Managed vs. Unmanaged VPS
I get asked whether it's worth paying for a managed VPS (where the provider handles OS updates, security, etc.). Here's my honest take:
| Unmanaged VPS | Managed VPS | |
|---|---|---|
| Monthly cost | $4–$10 | $20–$60+ |
| Setup time | 30–60 min | Near zero |
| Control | Full | Limited |
| Good for | Developers, indie hackers | Non-technical founders |
| Security | Your responsibility | Provider's responsibility |
If you're reading this post, you're probably technical enough to handle an unmanaged VPS. The $40–$50/month you save over managed hosting compounds fast — that's $480–$600/year you can put toward marketing, tooling, or just keeping the lights on longer.
Conclusion
Knowing how to set up a Linux VPS properly is one of those skills that pays for itself immediately and keeps paying. You spend 30 minutes doing it right once, and you don't spend 3 hours recovering from a breach six months later.
Here's what to do tomorrow: spin up the cheapest Hetzner VPS (€3.79/month), work through this guide top to bottom, and get comfortable with the process before you need it for a real project. The first time you do it takes 45 minutes. The tenth time takes 15.
Got a specific stack you're deploying — Rails, Django, a Node API? Drop a comment and I'll cover the app-specific parts in a follow-up post.