Most "how to set up a Linux VPS" guides stop at apt update && apt upgrade and call it a day. That's not a server. That's a root-login-enabled, no-firewall liability sitting on a public IP.
I've spun up probably 60-odd VPS instances over the years — Hetzner, DigitalOcean, Vultr, a few others. The first five minutes after provisioning are the ones that matter most. Get them right and you have a solid base. Get them wrong and you're one exposed port away from a cryptominer running on your bill.
This guide walks through exactly what I do every time I provision a fresh Linux VPS: from first SSH login to a production-ready baseline. No fluff, no "consider consulting your security team" disclaimers. Just the commands that work.
Choose Your Distro and Provider First
Before you touch a terminal, you need a machine. I run Ubuntu 22.04 LTS on almost everything. It's not the hippest choice, but the LTS support window runs to April 2027, package availability is excellent, and every Stack Overflow answer written in the last five years assumes you're on it.
For provider, I'm on Hetzner. A CX22 (2 vCPU, 4 GB RAM) costs €4.35/month as of mid-2025. DigitalOcean's equivalent Droplet is $18/month. The performance difference is negligible for most indie workloads. Pick Hetzner unless you have a specific reason not to.
Once you've provisioned the VPS, you'll get a root password or an SSH key option in the dashboard. Always choose SSH key. If your provider only offers a password, change that in step one below.
Step 1: First Login and Immediate Hardening
SSH in as root:
ssh root@YOUR_SERVER_IP
First thing — update everything:
apt update && apt upgrade -y
This takes 30-90 seconds. Don't skip it. Provisioning images are often weeks behind on patches.
Create a Non-Root Deploy User
Running everything as root is how you accidentally rm -rf the wrong directory with no guardrails. Create a dedicated user:
adduser deploy
usermod -aG sudo deploy
Now copy your SSH public key to the new user:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Open a second terminal and verify you can SSH in as deploy before closing your root session. Seriously — don't close root until you've confirmed this works.
Lock Down SSH
Edit /etc/ssh/sshd_config:
nano /etc/ssh/sshd_config
Change or add these lines:
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Changing the 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 it for the peace of mind alone.
Restart SSH:
systemctl restart sshd
Again — test the new config in a fresh terminal before closing anything:
ssh -p 2222 deploy@YOUR_SERVER_IP
Step 2: Set Up the Firewall
Ubuntu ships with ufw (Uncomplicated Firewall). It's simple enough to not get in your way and good enough for 99% of VPS use cases.
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 the rules:
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
If you're running a database like PostgreSQL or MySQL, do not open those ports publicly. Access them over localhost or a private network only. Every exposed database port is an invitation.
Step 3: Configure Swap Space
Hetzner's CX22 comes with 4 GB RAM and no swap by default. For most apps that's fine — until it isn't. A memory spike during a deploy or a traffic burst can OOM-kill your process with zero warning.
Add 2 GB of swap:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
Make it permanent across reboots:
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Then tune swappiness so the kernel doesn't reach for swap at the first sign of memory pressure:
sysctl vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.conf
The default swappiness is 60, which is too aggressive for a server. Setting it to 10 means the kernel only uses swap when RAM is genuinely under pressure.
Step 4: Set Hostname and Timezone
Small things, but they matter when you're reading logs at 2 AM.
hostnamectl set-hostname your-server-name
timedatectl set-timezone UTC
I always use UTC on servers. No daylight saving surprises, no confusion when correlating logs across services in different regions.
Update /etc/hosts to match the hostname:
nano /etc/hosts
Add a line like:
127.0.1.1 your-server-name
Step 5: Install Fail2Ban
Even with password auth disabled, bots will hammer your SSH port. Fail2Ban watches the auth logs and temporarily bans IPs that fail too many times.
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
Restart Fail2Ban:
systemctl enable fail2ban
systemctl restart fail2ban
Check it's running:
fail2ban-client status sshd
Step 6: Enable Automatic Security Updates
You're an indie hacker, not a sysadmin. You're not going to remember to patch your server every week. Let the machine do it.
apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades
Select "Yes" when prompted. This enables automatic installation of security updates only — it won't auto-upgrade your Node.js version or anything application-level. Safe and sensible.
Verify the config in /etc/apt/apt.conf.d/20auto-upgrades:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Step 7: Set Up a Basic Monitoring Baseline
You don't need Datadog. You need to know if your disk is full or your server is down.
Install htop for interactive process monitoring, and ncdu for disk usage:
apt install htop ncdu -y
For uptime monitoring, I use UptimeRobot on the free tier. It pings your server every 5 minutes and emails you if it goes down. Takes 2 minutes to set up. No agent to install.
For disk space alerts, add a cron job that emails you when disk usage crosses 80%:
crontab -e
Add:
0 8 * * * df -h / | awk 'NR==2 {if ($5+0 > 80) print "Disk usage: " $5}' | mail -s "Disk Alert: $(hostname)" you@example.com
This runs every morning at 8 AM. Simple, zero cost, no vendor dependency.
What Your Server Looks Like Now
Let's be concrete about what you've built:
| Layer | What You Did | Why It Matters |
|---|---|---|
| Access | Non-root user, SSH keys only | Eliminates password brute-force |
| Network | ufw with deny-by-default | Closes every port you didn't explicitly open |
| SSH | Custom port, root login disabled | Cuts automated scan noise |
| Resilience | Swap + tuned swappiness | Survives memory spikes without OOM kills |
| Automation | Fail2Ban + unattended-upgrades | Security without manual babysitting |
| Visibility | htop, ncdu, UptimeRobot | Know when something's wrong |
This is a baseline, not a finished product. What you install next — Nginx, Docker, Caddy, a Node app — depends on your use case. But the foundation is solid.
Common Mistakes to Avoid
Locking yourself out. Always test SSH access in a new terminal window before closing your existing session. I cannot stress this enough. Every experienced sysadmin has done this at least once.
Opening database ports to the internet. PostgreSQL on port 5432 should never be publicly accessible. If you need remote database access, use an SSH tunnel.
Skipping swap on small instances. A 1-2 GB RAM VPS without swap will OOM-kill processes under load. Swap buys you time to react.
Using a weak SSH key. If you generated an RSA key before 2020, it might be 1024-bit. Generate a fresh Ed25519 key: ssh-keygen -t ed25519 -C "your@email.com". It's faster and more secure than RSA 4096.
Not setting a timezone. UTC on servers, always. Your app logs will thank you.
Conclusion: How to Set Up a Linux VPS That's Actually Ready
Knowing how to set up a Linux VPS properly means more than just getting it online — it means getting it secure, observable, and maintainable before you deploy a single line of your application code.
The whole process above takes about 15-20 minutes on a fresh Ubuntu 22.04 instance. I've done it enough times that I have it in a shell script I run right after provisioning. You should too — even a rough Bash script beats repeating these steps from memory at 11 PM when you're spinning up a new server for a launch.
Tomorrow: provision a VPS (Hetzner CX22 if you want the best price-to-performance), run through these steps in order, and save your SSH config to ~/.ssh/config so you're not typing IPs forever. That's it. Everything else — your app stack, your CI/CD, your backups — can come after you have a solid base.