Everything to do after buying a VPS, in order. Each demo plays the disaster first, marked SKIPPED, then the same moment marked SET UP. Press play on any sketch to watch it happen.
The whole journey
1 From purchase to live site
A VPS from DigitalOcean, Hetzner, Vultr, Linode or similar is an empty Linux computer on the public internet. The company gives you an IP address and a login, and everything after that is your job. Commands on this page assume Ubuntu, and the order matters.
What you get: an IP address, a root login, and a bare operating system.
What you do not get: security, backups, HTTPS, monitoring or a way to deploy. Those are all yours.
Choosing a plan: an Ubuntu LTS image, a location near your users, and at least 1 GB of RAM for a small app.
Use an SSH key at purchase: most providers let you add one when creating the server.
Process diagram
1/5 You pay, and within a minute you get an empty server with a public IP.
Rule of thumb: Do the security steps before you deploy anything. Bots do not wait for you to finish.
First hour: lock the door
2 Log in and update everything
Your first job is to connect over SSH and install every security update. The image your provider used may be weeks old, and its known bugs are public.
Connect: use the IP address and login from your provider.
Update: apt pulls down the latest fixes. Reboot if the kernel changed.
Automatic updates: unattended-upgrades keeps installing security fixes after today.
1/5 You connect for the first time. The server works, so it feels finished.
If you skip this: bots scan the whole internet and try known bugs against old software. An unpatched server can be taken over without anyone guessing a password.
Rule of thumb: Take a provider snapshot once the setup is done, so you can rebuild in minutes.
3 SSH keys, no root, no passwords
Password login on port 22 is the first thing bots attack. Replace it with SSH keys, which cannot realistically be guessed, and work as a normal user who uses sudo.
Create a user: day-to-day work should not happen as root.
Use keys: your laptop holds a private key, and the server holds the public half.
Disable root and passwords: only after you have tested the new login.
adduser deploy
usermod -aG sudo deploy
# from your laptop
ssh-copy-id deploy@203.0.113.10
# in /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
sudo systemctl restart ssh
Skip vs set up
1/5SKIPPED root can log in with a password, and bots try thousands of them.
If you skip this: bots try common passwords against root around the clock. One good guess gives them total control of the server.
Rule of thumb: Keep your current SSH session open and test the new login in a second window. Otherwise one typo can lock you out.
4 Firewall: close every port you do not need
Any program listening on a public port can be reached by the whole world. A firewall blocks everything by default, and you open only what you mean to.
Open: SSH, plus 80 and 443 for the web. Usually nothing else.
Keep private: databases, admin panels, and your app's own port.
Docker warning: ports published with Docker can bypass UFW rules. Bind them to 127.0.0.1 instead.
Provider firewall: many companies offer one too, and using both is fine.
1/5SKIPPED with no firewall, every listening port faces the internet.
If you skip this: your database and admin ports sit open to the internet, and scanners find them within hours.
Rule of thumb: Run ufw allow OpenSSH before ufw enable, or you will lock yourself out.
5 Fail2ban: ban repeat offenders
Fail2ban watches your logs and blocks addresses that keep failing to log in. With SSH keys in place it is an extra layer, and it keeps your logs far quieter.
How it works: it reads login failures, then adds a temporary firewall ban.
Default on Ubuntu: installing it turns on protection for SSH.
Other services: jails can also protect Nginx and other logins.
1/4SKIPPED the attacker keeps trying, and nothing ever stops them.
If you skip this: attackers can hammer your login forever, filling your logs and burning CPU.
Rule of thumb: Add your own IP to ignoreip if it is fixed, so a few typos cannot ban you.
Put your app online
6 Point your domain at the server
Domain names are bought separately, from a registrar. You add an A record that points your domain at your VPS IP, and browsers can then find you by name.
A record: links a name such as example.com to an IPv4 address.
www: add a second record for www, or redirect it.
TTL: how long others may cache the answer. Changes can take minutes to hours to spread.
Type Name Value TTL
A @ 203.0.113.10 300
A www 203.0.113.10 300
# check it from your laptop
dig +short example.com
Skip vs set up
1/5SKIPPED a visitor types example.com, and DNS has no answer.
If you skip this: visitors must type a raw IP address, you cannot get a normal HTTPS certificate, and changing servers breaks every link.
Rule of thumb: Add DNS before HTTPS, because Let's Encrypt needs the domain to reach your server.
7 Keep the app running
Running your app in an SSH session ties its life to that session. A process manager like systemd, or Docker with a restart policy, starts it at boot and restarts it after a crash.
systemd: built into Ubuntu, and all you need for one app.
Docker Compose: good when you have several containers. Use restart: unless-stopped.
Environment files: keep secrets in a file only that user can read, never in Git.
Run as a normal user: a bug in your app should not hand out root.
1/6SKIPPED you start the app by hand in SSH. The site works, for now.
If you skip this: your site dies the moment you close your laptop, and stays down after every crash or reboot until you notice.
Rule of thumb: Reboot the server once on purpose. If the site does not come back alone, it is not set up yet.
8 Reverse proxy with Nginx
Your app listens on a private port such as 3000. Nginx, or Caddy, faces the internet on ports 80 and 443 and passes requests to it. One server can then host many sites.
Clean URLs: visitors use example.com, with no port number.
One front door: HTTPS, compression and rate limits all live in one place.
Static files: Nginx serves them directly, so your app does not have to.
Many sites: route by domain name to different apps on the same VPS.
1/5SKIPPED the app faces the internet by itself, as example.com:3000.
If you skip this: visitors see example.com:3000, there is nowhere sensible to add HTTPS, and your app takes every request from the internet directly.
Rule of thumb: Make the app listen on 127.0.0.1, so only Nginx can reach it.
9 HTTPS with Let's Encrypt
HTTPS encrypts everything between the browser and your server. Certificates from Let's Encrypt are free, and Certbot can install and renew them for you.
Certificate: proves that you control the domain.
Certbot: requests a certificate and edits your Nginx config.
Renewal: certificates are short-lived, so Certbot sets up automatic renewal.
Caddy: an alternative web server that handles HTTPS with almost no setup.
1/6SKIPPED the site runs on plain HTTP, and the browser warns "Not secure".
If you skip this: browsers label your site "Not secure", logins and cookies travel as readable text, and anyone on the same Wi-Fi can read or change them.
Rule of thumb: Run the renewal dry run once, so a silent failure cannot take your site down in three months.
10 Keep the database private
A database should talk only to your app, never to the internet. Bind it to localhost, give it a strong password, and connect through an SSH tunnel when you need to look inside.
Bind to localhost: PostgreSQL does this by default. Do not change it to all addresses.
Docker: publish with 127.0.0.1:5432:5432, or do not publish at all.
Separate user: give the app its own login with only the rights it needs.
Remote access: use an SSH tunnel instead of opening the port.
# docker-compose.yml: reachable from this machine only
ports:
- "127.0.0.1:5432:5432"
# look inside from your laptop, through SSH
ssh -L 5432:127.0.0.1:5432 deploy@203.0.113.10
Skip vs set up
1/6SKIPPED the database port was published to the internet to make testing easier.
If you skip this: attackers scan for open databases, then copy or wipe the data and leave a ransom note. Many exposed databases are hit this way.
Rule of thumb: Scan your own server from outside with nmap, and confirm that only the ports you meant are open.
Keep it alive
11 Memory and swap
Small servers run out of memory quickly. When that happens, Linux kills a process to save itself, and the victim is often your database. Swap gives you breathing room by using disk as slow overflow.
OOM killer: the part of Linux that picks a process to kill when memory runs out.
Swap: overflow space on disk. Slow, but much better than a crash.
Limits: cap memory for each container, so one app cannot starve the others.
Check: free -h shows memory, and dmesg shows whether the OOM killer has struck.
1/5 A normal day. Memory is 60% used and everyone is happy.
If you skip this: a traffic spike fills the memory, Linux kills your database, and the site goes down with nothing obvious in your app logs.
Rule of thumb: If the server uses swap all the time, upgrade the plan. Swap handles spikes, not steady shortage.
12 Backups, somewhere else
A VPS is one disk in someone else's building. Backups must live outside that server, ideally with another company, and they only count once you have restored from them.
Automate: a scheduled job, because manual backups stop happening.
Off-site: send copies to object storage, such as S3 or Backblaze B2.
Snapshots: provider snapshots are handy, but they share your account and your provider.
Test restores: practise on a spare server. An untested backup is only a hope.
Encrypt: a backup holds all your data, so protect it like the database.
#!/bin/sh
# /usr/local/bin/backup.sh
pg_dump -U app shop | gzip > /tmp/shop.sql.gz
rclone copy /tmp/shop.sql.gz b2:my-backups/
rm /tmp/shop.sql.gz
# crontab -e: every night at 02:30
30 2 * * * /usr/local/bin/backup.sh
Skip vs set up
1/6SKIPPED all your data sits on one disk, and everything seems fine.
If you skip this: a failed disk, a bad command or a hacker can erase everything, permanently. Most providers do not keep backups unless you pay for them.
Rule of thumb: Follow 3-2-1: three copies, on two kinds of storage, with one off-site.
13 Monitoring, logs and disk space
Nobody is watching your server unless you arrange it. You need something outside the server to check that it is up, and alerts for the slow problems, like a disk filling with logs.
Uptime check: an outside service that requests your site every minute and alerts you.
Resource alerts: warn at about 80% disk and memory, while there is still time to act.
Self-hosted: tools like Uptime Kuma are good, but should not run on the server they watch.
# how full is the disk?
df -h
# what is using it?
sudo du -h /var --max-depth=2 | sort -h | tail
# cap Docker logs in /etc/docker/daemon.json
{ "log-opts": { "max-size": "10m", "max-file": "3" } }
Skip vs set up
1/6SKIPPED logs grow every day, and nobody is looking.
If you skip this: you find out the site is down from an angry customer. A full disk can silently stop your database from writing.
Rule of thumb: A monitor on the same server dies along with the server. Keep your uptime check elsewhere.
14 Automatic deploys
Copying files by hand works once and fails later. A pipeline makes every deploy the same: test, build, send to the server, restart, check it is healthy.
GitHub Actions: runs on merge, using an SSH key stored as a secret.
A limited key: give CI its own deploy user, not your personal key.
Versioned images: tag each release so going back is one command.
Health check: after deploying, confirm the site answers before calling it done.
# .github/workflows/deploy.yml (the last job)
- name: Deploy
run: |
ssh deploy@203.0.113.10 '
cd app &&
docker compose pull &&
docker compose up -d &&
curl -f http://127.0.0.1:3000/health
'
Skip vs set up
1/6SKIPPED you copy files to the server by hand, from memory.
If you skip this: every release relies on memory. One forgotten step takes the site down, and there is no quick way back.
Rule of thumb: If only one person knows how to deploy, you do not have a deploy process yet.
15 When one VPS is not enough
One well-set-up VPS can carry a surprising amount of traffic, and it is cheap and simple. It is still a single machine, though, so plan what happens when it fails or fills up.
Single point of failure: if the host dies, so does everything on it.
First upgrade: a bigger plan is the quickest step.
Split the database: move it to its own server or a managed service.
Then scale out: add a second app server behind a load balancer.
Deployment diagram
1/5 Start here: one VPS holds the app and the database. Cheap and simple.
Rule of thumb: Stay on one VPS until numbers say otherwise. The AWS page shows the next steps.