← Back to GuidesGUIDESelf-Hosting & Homelab

Setting Up Your Own VPS: A Secure Starting Point

28 minutes ago
❤️ 0 likes
💬 0 comments
dockerself-hostingcode-securityvpssshlinux
Setting Up Your Own VPS: A Secure Starting Point

Every self-hosted project I run starts the same way: a brand new VPS and about twenty minutes of setup before I install a single application. That twenty minutes is what separates "my server" from "someone else's crypto miner." A fresh box with a public IP starts getting probed within minutes, and the default configuration on most images is built for convenience, not safety.

This is the secure baseline I set up on every new server, before Docker, before n8n, before anything else. It is also the starting point our production n8n guide assumes you already have. Every command below was checked against current Ubuntu LTS documentation, and I flag the parts that genuinely need a real server to verify.

Tip

Key takeaways Never do daily work as root. Create a sudo user and log in as that instead.Use an SSH key and turn password login off, but only after you confirm the key works.Deny everything at the firewall by default, then open only the ports you actually use.Turn on automatic security updates so patches land while you sleep.If you plan to run Docker, remember that published ports skip UFW. Bind them to 127.0.0.1.

Prerequisites

  • A VPS running a current Ubuntu LTS. Both 24.04 "Noble Numbat" and 26.04 "Resolute Raccoon" work well. I run long-lived boxes on Hostinger VPS hosting, which is also what powers the n8n guide.
  • An SSH key pair on your own machine. If you do not have one yet, Step 3 creates it.
  • A terminal, and a note of your provider's recovery console. Most hosts, Hostinger included, give you a browser based console in their control panel. That is your way back in if you ever lock yourself out, so find it before you start.

Info

Disclosure: some links in this guide, including the Hostinger link above, are referral or affiliate links. If you sign up through them we may earn account credit or a commission, at no extra cost to you. We only point at tools we actually run.

Step 1: Log in and update the system

Right after the server boots, log in with the credentials your provider gave you and bring every package up to date:

Bash
1ssh root@YOUR_SERVER_IP2apt update && apt upgrade -y

If the upgrade pulls a new kernel, reboot with reboot and log back in. Starting from a fully patched system means the rest of this guide is the only thing left between you and a solid baseline.

Step 2: Create a non-root user

Working as root all day is the single most common mistake on a new server. One typo or one bad script runs with full control of the machine. Create a normal user with sudo rights and use that from now on:

Bash
1adduser deploy2usermod -aG sudo deploy

Swap deploy for whatever name you like. The adduser command asks for a password; pick a strong one, since sudo will ask for it.

Step 3: Set up SSH keys

From your own machine, not the server, create a key if you do not already have one:

Bash
1ssh-keygen -t ed25519 -C "you@your-machine"

Copy the public half up to the new user, then log in as that user to confirm it works:

Bash
1ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@YOUR_SERVER_IP2ssh deploy@YOUR_SERVER_IP

Do not move on until that last command logs you in without asking for the account password. The next step turns password login off completely, and if the key is not working you will lock yourself out.

Step 4: Harden SSH

Now lock SSH down to keys only and stop root from logging in over it. On Ubuntu, the clean way is a drop-in file, so a future package update cannot quietly overwrite your changes. As the deploy user:

Bash
1sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null << 'EOF'2PermitRootLogin no3PasswordAuthentication no4KbdInteractiveAuthentication no5PubkeyAuthentication yes6MaxAuthTries 37LoginGraceTime 308X11Forwarding no9EOF

There is one catch that trips people up. SSH reads its settings top to bottom and keeps the first value it finds for each one, and cloud images ship a lower numbered file that sets PasswordAuthentication for you. On Ubuntu 24.04 it is 60-cloudimg-settings.conf, which already sets it to no; older images used 50-cloud-init.conf and sometimes set it to yes. Because a lower numbered file loads first, whatever it says wins over your file. So do not trust the filename alone. Check the effective configuration, which is the real source of truth:

Bash
1sudo sshd -t   # tests syntax; no output means it is fine2sudo sshd -T | grep -E "permitrootlogin|passwordauthentication|pubkeyauthentication"

You want to see passwordauthentication no and permitrootlogin no in that output. If password auth still says yes, open the offending lower numbered file, comment that line out, and check again. Once it reads correctly, reload SSH (this does not drop your current session):

Bash
1sudo systemctl reload ssh

Keep your existing terminal open and log in from a second one to be sure. If anything is wrong, the open session is your safety net.

Step 5: Turn on a firewall

UFW is a friendly front end to the kernel firewall. It is present on the desktop and full server images, but a minimal cloud image often does not include it, so install it first (a no-op if it is already there). Then set it to deny everything coming in, allow your own traffic out, and open only SSH and the web ports:

Bash
1sudo apt install -y ufw2sudo ufw default deny incoming3sudo ufw default allow outgoing4sudo ufw allow OpenSSH5sudo ufw allow 80,443/tcp6sudo ufw enable7sudo ufw status verbose

If you are not serving a website yet, skip the 80,443 line and add it later. The whole idea is that nothing is reachable unless you said so.

Step 6: Automatic security updates

Security patches are only useful once they are installed. The unattended-upgrades package applies them for you:

Bash
1sudo apt install unattended-upgrades -y2sudo dpkg-reconfigure --priority=low unattended-upgrades

Choose "Yes" at the prompt. You can preview what it would do without changing anything:

Bash
1sudo unattended-upgrades --dry-run --debug

By default it installs security updates only, which is the sweet spot: you stay patched without surprise changes to everything else on the box.

Step 7: (Optional) Add fail2ban

With password login already off, brute force attempts against SSH are mostly noise, since there is no password to guess. If you still want to trim the log spam, fail2ban watches for repeated failures and bans the source for a while:

Bash
1sudo apt install fail2ban -y

Its defaults enable an SSH jail out of the box. Treat this as a nicety, not a substitute for keys and a firewall.

The Docker trap almost everyone hits

Here is the one that surprises even experienced people, and it is why it belongs in the baseline.

Warning

Docker bypasses UFW. When you publish a container port, Docker writes its own firewall rules that skip UFW entirely. Your ufw status can look locked down while a database sits wide open to the internet.

The reason is where each tool sits in the network path. Docker sends published-port traffic through its own chain before it ever reaches the point UFW inspects, so UFW never gets a say. This is documented behavior, described in Docker's own packet filtering and firewalls page and flagged by the OWASP Docker Security Cheat Sheet.

The simplest, most reliable fix is to bind published ports to localhost instead of every interface. Compare:

YAML
1# Exposed to the whole internet, even with UFW "on":2ports:3  - "5432:5432"4 5# Reachable only from the server itself:6ports:7  - "127.0.0.1:5432:5432"

Better still, do not publish internal services at all. Containers on the same Docker network reach each other by name, so a database that only your app talks to needs no host port. That is exactly the pattern in our n8n guide, where Postgres is never published and only Caddy faces the internet.

Where to go next

You now have a server that is patched, key-only, firewalled, and no longer running as root. Two natural next steps:

An honest word on trade-offs

None of this makes a server "unhackable," and anyone who tells you a checklist does is selling something. What it does is remove the easy wins: default passwords, root over SSH, exposed services, unpatched holes. That covers the overwhelming majority of automated attacks, which is what actually hits a small VPS. Keep your software updated, keep backups you have tested, and add depth (like the Tailscale step) as your setup grows.

Final thoughts

The shape of this never really changes: a real user, keys not passwords, a default-deny firewall, automatic patches, and an awareness of how Docker treats ports. Do it once, turn it into muscle memory, and every future box takes ten minutes. Then you get to the fun part, which is running your own software.

Verified on a real Ubuntu 24.04.4 server with Docker 29.1.3: the SSH hardening effective values (`sshd -T`), the default-deny UFW policy, the Docker-bypasses-UFW behavior and the `127.0.0.1` fix (an external request reached the `0.0.0.0`-published port straight through an active firewall, then was refused once the port was bound to loopback), automatic security updates, and the fail2ban SSH jail. See the linked lab notes.

Join the discussion on Setting Up Your Own VPS: A Secure Starting Point

Likes, comments, and replies are available for authenticated readers with verified email addresses.

Comments (0)

Loading discussion...

Related guides