Most tutorials on how to set up a Linux VPS stop at "log in as root and install stuff." That's not a setup. That's a ticking clock before something goes wrong — a brute-forced password, a world-readable config file, or a runaway process eating your RAM at 3 a.m.
I've made every one of those mistakes across nine years of running production servers. This guide is what I actually do when I spin up a new VPS today — on Hetzner, where a CAX11 (2 vCPU ARM, 4 GB RAM) runs you €3.79/month as of mid-2024. The steps work on any Ubuntu 22.04 or Debian 12 box regardless of provider.
By the end you'll have a hardened server, a non-root deploy user, UFW firewall rules, and a simple Nginx install to prove the whole thing works. Let's get into it.
Step 1: Create the VPS and Get Root Access
Pick your provider, spin up Ubuntu 22.04 LTS, and grab the root password or SSH key from the control panel. I always choose SSH key auth at creation time if the panel supports it — Hetzner, DigitalOcean, and Vultr all do.
If you're stuck with a password, your first SSH session looks like this:
ssh root@YOUR_SERVER_IP
You'll be prompted to change the root password on first login on some providers. Do it. Use a password manager to generate something 32+ characters long. You won't type it manually again after this step anyway.
Once you're in, update everything before touching anything else:
apt update && apt upgrade -y
On a fresh Hetzner box this takes about 90 seconds. Don't skip it — you don't want to build on top of packages with known CVEs.
Step 2: Create a Non-Root User
Running as root is the server equivalent of doing woodworking with no safety glasses. Sure, it's fine until it isn't.
Create a deploy user and give it sudo:
adduser deploy
usermod -aG sudo deploy
Now copy your SSH public key to the new user so you can log in without a password:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Open a second terminal and test the login before you close your root session:
ssh deploy@YOUR_SERVER_IP
If that works, you're good. Don't close the root session yet — you'll need it in the next step.
Step 3: Harden SSH
This is the step most tutorials skip or bury at the end. Do it now, while you still have two sessions open.
Edit /etc/ssh/sshd_config:
sudo nano /etc/ssh/sshd_config
Change or add these lines:
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
X11Forwarding no
AllowUsers deploy
A few notes on these choices:
- Port 2222 isn't security through obscurity as a primary defense — it just cuts 90%+ of automated SSH scanners from your logs. Worth it for the noise reduction alone.
- PermitRootLogin no means even if someone gets root's key, they can't SSH in directly.
- PasswordAuthentication no kills brute-force attacks dead. Non-negotiable.
Restart SSH:
sudo systemctl restart sshd
Now test the new config from your second terminal:
ssh -p 2222 deploy@YOUR_SERVER_IP
If that works, close the root session. If it doesn't, you still have the root session to fix things.
Step 4: Configure the Firewall with UFW
UFW (Uncomplicated Firewall) ships with Ubuntu and wraps iptables in a sane interface. It's not the most powerful option — if you need fine-grained iptables rules or nftables, use those — but for most indie projects it's exactly enough.
Install and set defaults:
sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
Allow your custom SSH port and HTTP/HTTPS:
sudo ufw allow 2222/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Enable it:
sudo ufw enable
Verify:
sudo ufw status verbose
You should see your three rules and "Status: active." If you're running a database like Postgres, do not open port 5432 to the world. Keep it local and connect via SSH tunnel or a private network interface.
Step 5: Install Fail2ban
Even with password auth off, you'll get connection attempts on port 2222. Fail2ban watches your auth logs and auto-bans IPs after repeated failures.
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Create a local config so package updates don't overwrite your settings:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo 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
Restart fail2ban:
sudo systemctl restart fail2ban
Check it's working:
sudo fail2ban-client status sshd
You should see the jail is active with 0 currently banned IPs. Give it 24 hours and that number will be nonzero — the internet never sleeps.
Step 6: Set Up Automatic Security Updates
I used to manually run apt upgrade on all my servers. Then I had four servers. Then six. Manual updates don't scale, and skipping them is how you end up on a botnet.
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Answer yes when prompted. This configures automatic installation of security updates only — it won't auto-upgrade your Node.js from 18 to 20 and break your app. That's the right tradeoff.
Verify the config:
cat /etc/apt/apt.conf.d/20auto-upgrades
You should see APT::Periodic::Unattended-Upgrade "1";.
Step 7: Install Something and Prove It Works
A hardened server that serves nothing is just an expensive hobby. Let's install Nginx to confirm the whole stack is working end to end.
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
Open your browser and go to http://YOUR_SERVER_IP. You should see the Nginx welcome page.
If you want to serve a real app, you'll typically run your app on a local port (say, 3000) and proxy through Nginx. Here's a minimal config for that — save it to /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable it:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
For HTTPS, add Certbot:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com
Certbot will edit your Nginx config and set up auto-renewal. Free TLS in under two minutes. This is one of those things that used to cost real money and now doesn't.
Comparing Common VPS Providers for This Setup
The steps above work everywhere, but the value you get per dollar varies a lot.
| Provider | Cheapest Plan | RAM | vCPU | SSD | Notes |
|---|---|---|---|---|---|
| Hetzner CX22 | €3.79/mo | 4 GB | 2 | 40 GB | Best value in Europe, AMD EPYC |
| DigitalOcean Basic | $6/mo | 1 GB | 1 | 25 GB | Reliable, worse specs per dollar |
| Vultr Cloud Compute | $6/mo | 1 GB | 1 | 25 GB | Similar to DO, more data center locations |
| Linode/Akamai | $5/mo | 1 GB | 1 | 25 GB | Good network, pricing unchanged for years |
| Oracle Cloud Free | $0 | 1 GB | 1 | 47 GB | Free tier exists, ARM option too, but support is rough |
I run everything on Hetzner because the specs-per-euro ratio is genuinely hard to beat. Oracle's free tier sounds great until you need to actually talk to their support team.
What to Do Next After Your Linux VPS Is Running
Once you know how to set up a Linux VPS and have the basics locked down, the natural next steps are:
- Set up swap if you're on a 1-2 GB RAM box. Apps like Ruby on Rails will OOM-kill themselves without it.
- Configure log rotation so
/var/logdoesn't fill your disk in six months. - Add monitoring. Even a free UptimeRobot check is better than finding out your server is down from a user tweet.
- Automate your deploys. SSH-ing in to
git pull && pm2 restartby hand gets old fast. Look at GitHub Actions with an SSH deploy step, or Kamal if you're containerizing.
For a deeper look at keeping your server costs predictable, check out how to monitor VPS resource usage without paid tools — it covers free options that actually work.
If you're planning to run multiple apps on this box, setting up Nginx as a reverse proxy for multiple domains is the logical next read.
Conclusion: Your Linux VPS Checklist
Here's the full picture of how to set up a Linux VPS securely in one place:
- Spin up Ubuntu 22.04, update packages immediately
- Create a non-root user with sudo and SSH key access
- Harden SSH: custom port, no root login, no password auth
- Enable UFW with deny-by-default and only open what you need
- Install and configure Fail2ban on your SSH port
- Enable unattended security upgrades
- Install your app stack and verify it serves traffic
This takes about 45 minutes the first time. After that you'll have a mental script and do it in 15. The time investment pays for itself the first time you don't get compromised.
Tomorrow: spin up a $4 Hetzner box and run through this list. If you already have a VPS that skipped some of these steps, audit it against this checklist tonight. The SSH hardening step alone is worth doing right now.