This will be a straightforward and brief post on how to setup and harden a basic VPS for your "million dollar idea" side projects. If you dislike cloud providers like I do, you are probably looking for a simple and cheap alternative. For that matter, you've probably heard of Hetzner, which I will be using for this example.
The cheapest Hetzner server right now is €4.99/month and gives you 2 virtual cores, 4GB of RAM and 40GB of NVMe SSD - which is probably enough to host your side projects, however you are free to pay a little more depending on your budget (and confidence). As much as everyone else, I also love to dream that someday my apps will reach millions of users - but realistically they will most likely never come close, so this small box will cover my needs for now. If this ends up not being enough for you, then you've probably already made enough money to pay someone else (or a cloud provider) to worry about scaling.
Intro
I am not a Linux guru, however I understand that there are a few basic steps I need to take before I actually deploy my app to production. The first is hardening whatever server I am deploying on to make sure I don't get hacked - especially considering AI models are getting better and better at cracking all kinds of seemingly secure software. I will do my best to explain why each step is being done so you can at least understand the reasoning, rather than just copy-pasting some magical Linux spell.
Prerequisites
1. Domain
I assume you already have this covered. Personally I just use Cloudflare, but they do have some annoying limitations around DNS that force you to use their nameservers - for now I don't mind.
2. SSH key
Open your preferred terminal and type ssh-keygen. Follow the instructions on screen and you should end up with a key at ~/.ssh/id_<keyname>.pub.
The steps
1. Provision the server
This is pretty self-explanatory. If you are not a fan of Hetzner you can use whatever provider you like, just be realistic and don't go overboard.
During provisioning, Hetzner will ask for an SSH key - which we now have at ~/.ssh/id_<keyname>.pub. Once the server is up, configure a DNS A record pointing to its IP address. I would also suggest adding a CNAME record pointing www.example.com to example.com.
Installing an LTS Linux version is as simple as selecting it from a dropdown and waiting a couple of minutes. Easy peasy.
Installing an LTS Linux version is as simple as selecting it from a dropdown and waiting a couple of minutes. Easy peasy.
2. Log in
We use ssh to log into servers from the terminal. It's basically a shell wrapped in a similar encryption protocol to the https websites that dominate the web these days.
Since you already gave the server your public key, no password is needed. Just open a terminal and type:
ssh <IP> -l root
If successful, you should see something like root@ubuntu-2604 ~ #.
3. First steps
These are basic hygiene steps you would do on any Linux machine - update installed packages to their latest versions and give the server a name. On Ubuntu the package manager is called apt. The first line updates its sources, the second installs the latest versions. The -y flag skips the confirmation prompts.
apt update apt upgrade -y
Then set the hostname (useful when the server sends you notifications):
hostnamectl set-hostname server.example.com
4. Set up a non-root user
You are currently logged in as root, which you can tell from the root@ubuntu-2604… prompt. The root user has absolute access to the system and can famously destroy it by running rm -rf / (don't do it). The general practice is to work with a non-root user whose permissions are scoped to what it actually needs.
To create one, run:
To create one, run:
adduser yourusername
Take note of the password you set here - you will need it to run privileged commands with sudo. Since we trust ourselves (hopefully), we'll grant this user sudo privileges:
usermod -aG sudo yourusername
Then copy the SSH key over so you don't need a password to log in:
rsync --archive --chown=yourusername:yourusername ~/.ssh /home/yourusername
Open a new terminal tab and test the login:
ssh yourusername@<IP>
You should see: yourusername@server:~$
Test that sudo works:
sudo apt update
And while you're here, install your favourite editor - mine is neovim:
sudo apt install neovim
5. SSH Hardening
Is SSH easy to hack? No. But it's not impossible, and this server will be around for years, so why take chances.
SSH runs on port 22 by default, which is one of the first ports anyone trying to break into a server will scan. Moving it to a high-numbered port won't make you unhackable, but it will make you invisible to the majority of automated scripts that don't bother scanning beyond the first 1024 ports.
Open /etc/ssh/sshd_config with your editor and make the following changes:
Change the port:
Port 12444
Port 12444
Disable root login - we have a real user now, root should never be logging in:
PermitRootLogin no
PermitRootLogin no
Disable password authentication - SSH keys are strictly better:
PasswordAuthentication no
PasswordAuthentication no
Whitelist your user - why would anyone else need to log in anyway:
AllowUsers yourusername
AllowUsers yourusername
Now restart SSH. Importantly, your current session will stay open, so if something went wrong you can fix it without reinstalling everything:
sudo systemctl restart ssh sudo systemctl daemon-reload sudo systemctl restart ssh.socket
Without closing your current session, open a new tab and verify that the following fail:
ssh <IP> # should fail on port 22 ssh <IP> -p 12444 -l root # root login should be denied
And that this works:
ssh <IP> -p 12444 -l yourusername
One last thing - add this to ~/.ssh/config on your local machine so you don't have to type all that every time:
Host server
Hostname <IP>
Port 12444
User yourusername
ServerAliveInterval 120
ServerAliveCountMax 4
Hostname <IP>
Port 12444
User yourusername
ServerAliveInterval 120
ServerAliveCountMax 4
Now you can just type ssh server.
6. Firewall with ufw
6. Firewall with ufw
A firewall controls what traffic is allowed in and out of your server. Linux ships with ufw (uncomplicated firewall) which does the job without any drama.
We only need three ports open: your SSH port, HTTP and HTTPS. That's it - with just these three you can serve as many apps as you want.
sudo ufw allow 12444/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable
7. Automatic security updates
Vulnerabilities get found, patches get released. If you don't install them, your server slowly becomes a liability. The simplest solution is to just automate it.
sudo apt install -y unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades
Then edit /etc/apt/apt.conf.d/50unattended-upgrades and set:
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";
To actually receive those emails you need to set up a mailer. The easiest way is via Gmail with an app password:
sudo apt install nullmailer sudo apt install mailutils
Edit /etc/nullmailer/remotes and set:
Then restart:
sudo chmod 600 /etc/nullmailer/remotes sudo systemctl restart nullmailer
Test it:
echo "Test body" | mail -s "Test subject" [email protected]
8. Fail2ban
Even with everything locked down, someone could still hammer your SSH port hoping to get lucky. fail2ban watches the auth logs and automatically bans IPs that misbehave.
sudo apt install -y fail2ban sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local, find the port line under [sshd] and set it to your custom SSH port:
port = 12444
Restart and verify:
sudo systemctl restart fail2ban sudo fail2ban-client status sshd
Finally
If you made it this far, you now have a production-ready server for the price of peanuts. No devops team or cloud nonsense. A lot of people overcomplicate this. The steps above are genuinely all you need for 99% of side projects - anything more than this is most likely overkill.
Stay pragmatic. Your app does not need Kubernetes, it does not need auto-scaling, it does not need to be spread across three continents. It needs to work, and a €5 box running Docker will make it work just fine.