How to Set Up a Linux VPS from Scratch (2024)

by David Park
How to Set Up a Linux VPS from Scratch (2024)

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/log doesn'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 restart by 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:

  1. Spin up Ubuntu 22.04, update packages immediately
  2. Create a non-root user with sudo and SSH key access
  3. Harden SSH: custom port, no root login, no password auth
  4. Enable UFW with deny-by-default and only open what you need
  5. Install and configure Fail2ban on your SSH port
  6. Enable unattended security upgrades
  7. 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.