Setting up a VPS
I currently use PythonAnywhere for website hosting, ExpressVPN, and occasionally ngrok to expose test sites on my HOME machine (a Windows desktop) and facilitate RDP access to HOME when I’m traveling. All of these services work well, but I’ve been wondering if there is a better solution. Chatters suggested a VPS—virtual private server—as a way to combine all these services in one, retain greater control, and lower my total monthly cost. This post describes what I’ve set up and how it is used—mostly for future me.
A companion post describes how I’m using the VPS for website hosting, and another describes DNS records.
As always, lots of help from GPT5: an invaluable time-saver.
Provisioning
SSH and security
Logging in…
passphrase on HOME
After setup
sudo timeout fix
Firewalls
# ufw (Ubuntu default)
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 51820/udp
sudo ufw enable
sudo ufw status
steve@VPS1:~$ sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
51820/udp ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
51820/udp (v6) ALLOW Anywhere (v6)
Software installation
rg, emacs, uv
sudo apt install emacs -y
chmod
and all that
uv
Python
caddy
Terminology:
- VPS = your public front door.
- Caddy (on the VPS) = the receptionist: answers on ports 80/443, gets free HTTPS, and sends each request to the right “office.”
- WireGuard = a private hallway from the VPS to your home PC (e.g., VPS = 10.0.0.1, home PC = 10.0.0.2).
- Backends = your Flask servers (either running on the VPS itself, or on your home PC via the private hallway).
For each name you want Caddy to handle, point DNS A (IPv4) and AAAA (IPv6) to the VPS IPs.
If a site runs on the VPS: Caddy proxies to 127.0.0.1:PORT
. If a site runs at home: Caddy proxies to your WireGuard IP, e.g. 10.0.0.2:PORT
.
Open ports on the VPS: allow TCP 80/443 (HTTP/HTTPS) for Caddy and UDP (WireGuard port, e.g. 51820) so the private hallway stays up.
Caddyfile (readable template)
Below is a simple Caddyfile with explanations inline.
# /etc/caddy/Caddyfile
# Caddy auto-fetches TLS certs for each site below.
# --- mynl.com (public) ---
# Serve both apex and www; redirect www -> apex (or flip it if you prefer).
mynl.com, www.mynl.com {
@www host www.mynl.com
redir @www https://mynl.com{uri} # canonicalize
encode gzip
# OPTION A: Flask runs on the VPS itself:
# reverse_proxy 127.0.0.1:8000
# OPTION B: Flask runs at home via WireGuard:
reverse_proxy 10.0.0.2:8000
}
# blog.mynl.com lives on GitHub Pages — handled by DNS CNAME, not by Caddy.
# --- convexrisk.com (public) ---
convexrisk.com, www.convexrisk.com {
@www host www.convexrisk.com
redir @www https://convexrisk.com{uri}
encode gzip
# Choose one option as above:
# reverse_proxy 127.0.0.1:8100
reverse_proxy 10.0.0.2:8100
}
# --- app subdomains on your home PC via WireGuard ---
# Just add one site block per appX with its port.
app1.mynl.com {
encode gzip
reverse_proxy 10.0.0.2:5001
}
app2.mynl.com {
encode gzip
reverse_proxy 10.0.0.2:5002
}
# …repeat for app3.mynl.com → 10.0.0.2:5003, etc.
Explanation:
mynl.com, www.mynl.com { … }
: “this block handles both names.”The
redir
rule: sendswww
traffic to the apex (tidy URLs).encode gzip
: compress responses automatically.reverse_proxy …
: the one line that decides where requests go:127.0.0.1:PORT
→ app on the VPS.10.0.0.2:PORT
→ app on your home PC over WireGuard.
One site block per subdomain is the simplest, most explicit setup.
Certificates (TLS)
Great spot — this part is really important, and Caddy makes it magical.
TLS certs the old way (painful)
TL;DR: DNS control + challenge-response = secure proof of ownership.
To run a secure site (https://…
), the server needs a TLS certificate. Traditionally you’d:
- Prove to a Certificate Authority (CA) that you own the domain.
- Download cert files (
.crt
,.key
). - Install them in Nginx/Apache config.
- Remember to renew them before they expire (often every 90 days with Let’s Encrypt).
- Deal with errors if renewal fails.
That’s the fiddly, error-prone bit system admins used to spend time on. Caddy makes it easy by:
- You add a domain block in the Caddyfile (e.g.
mynl.com { … }
). - Caddy sees that domain and says: “I need a cert for this.”
- Caddy talks directly to Let’s Encrypt (a free CA).
- Let’s Encrypt says: “Prove you control
mynl.com
by answering a special challenge.”- The challenge is a temporary file served on
http://mynl.com/.well-known/acme-challenge/...
. - Because your DNS already points to the VPS, and port 80 is open, Let’s Encrypt can fetch that file from Caddy.
- The challenge is a temporary file served on
- Once validated, Let’s Encrypt issues a certificate.
- Caddy stores it in its local directory and automatically reloads it into use.
- Renewals: every ~60 days, Caddy silently re-does the process, so your certs never expire.
The net result: every domain you add (mynl.com
, convexrisk.com
, app1.mynl.com
, …) gets a free, valid, auto-renewed HTTPS cert. Browsers trust Let’s Encrypt, so your users see the padlock. You don’t have to learn certificate management — Caddy removes that whole layer.
Logs & reloads
- Caddy runs as a service; after editing the file:
caddy reload
(applies changes instantly). - If a backend is down, Caddy returns 502; bring the app up, reload, and you’re good.
Remote Desktop (RDP) note
RDP is not HTTP, so Caddy doesn’t proxy it. Two safe patterns:
- Best: use WireGuard only. From your laptop (on the road), connect the VPN, then RDP to
10.0.0.2:3389
directly — never expose RDP to the public internet. - If you must bounce via the VPS: use a TCP proxy (e.g., HAProxy) on the VPS to forward a high, firewall-restricted port →
10.0.0.2:3389
. Still keep it VPN-only or IP-allowlisted.
WireGuide
If you open port 3389 on your home router to the internet anyone in the world can try to connect to your Windows login. Bots constantly scan the internet for open RDP and brute-force passwords.Even with strong passwords, exploits have historically been found in RDP itself. That’s why exposing RDP directly is considered dangerous.
The safe pattern: put RDP behind a tunnel or VPN. You can use WireGuard to do this. Install WireGuard on your home PC (and iPhone/iPad) and on the VPS. When you want to RDP connect:
- Open the WireGuard app → it makes a secure tunnel from your device to your VPS → back home to your PC.
Now your iPhone/laptop is “virtually” on your home network. You open Microsoft’s Remote Desktop app and connect to 10.0.0.2:3389
(your PC’s private VPN address). To the world, port 3389 is never exposed. Only WireGuard’s port (a single UDP port like 51820) is open — and that requires cryptographic keys to talk. The security gain: RDP is only reachable after the VPN is up. Even if someone scans your VPS, they’ll never see RDP.
On iPhone/iPad you open the WireGuard app, flip the tunnel switch → VPN on. Open the Microsoft Remote Desktop app, connect to your home PC → you’re in.
choco install wireguard
sudo apt install wireguard -y
Exactly — on the VPS it’s usually just a one-liner with sudo
, because all the main Linux distros package WireGuard now.
Here’s the shape depending on distro:
On Ubuntu / Debian VPS
sudo apt update
sudo apt install wireguard -y
That gives you:
wg
→ command to inspect/add keys and peers.wg-quick
→ simple helper to bring configs up/down (wg-quick up wg0
).
Generate keys umask 077
means “only I (the owner) can read/write; no one else.” tee privatekey
saves a copy of that secret into a file called privatekey
.
umask 077
wg genkey | tee privatekey | wg pubkey > publickey
privatekey
→ keep safe on the VPS and publickey
→ share with your PC/phone. Next, create a config file (e.g. /etc/wireguard/wg0.conf
) with:
- VPS’s private key.
Address = 10.0.0.1/24
(VPS’s VPN IP).ListenPort = 51820
.- One
[Peer]
block per client (PC, phone).
Finally, bring it up
sudo wg-quick up wg0
And enable at boot:
sudo systemctl enable wg-quick@wg0
Editing /etc/wireguard/wg0.conf
sudo nano /etc/wireguard/wg0.conf
# or
sudo emacs /etc/wireguard/wg0.conf
Sample wg0.conf
file
[Interface]
PrivateKey = (your VPS’s private key)
Address = 10.0.0.1/24
ListenPort = 51820
[Peer]
# Windows PC
PublicKey = (PC’s public key)
AllowedIPs = 10.0.0.2/32
[Peer]
# iPhone
PublicKey = (iPhone’s public key)
AllowedIPs = 10.0.0.3/32
VPN
Linux editors
WinSCP and Sublime
VPN
VPS, HOME, phone
RDP
VPS, HOME, phone
Security
A VPS is a virtual machine: your “own server” running inside a bigger physical server owned by the provider. That physical server is managed by the host (Hetzner, DigitalOcean, etc.), and they control the underlying hypervisor (the virtualization software).
The host can see inside in principle: the host admins have full control of the hypervisor. They could pause your VM, mount its virtual disk, and read its contents. But in practice: reputable VPS providers don’t snoop. Their business depends on trust, and doing so would break the law (privacy, data protection). Think of it like renting an apartment in a building: You have your own door and keys. The landlord could enter with the master key — but they’re not supposed to, and doing so without permission is illegal.
They definitely can see Traffic leaving your VPS: since they provide the networking, they can log metadata (IP addresses, ports, traffic volumes) and resource usage: CPU, RAM, disk, network throughput.
They can’t normally see Your app-level secrets: SSH keys, database contents, files — unless they break into your VPS. VPN traffic contents: if you use WireGuard/HTTPS, your data is encrypted before it ever leaves the VPS.
Background notes
A VPS can replace PythonAnywhere and ngrok.
The big difference is control vs. convenience:
PythonAnywhere gives you a managed Python web hosting environment. You upload a Flask/Django app, click a few buttons, and it runs. You don’t manage the OS, networking, or updates — they do.
VPS gives you a raw Linux box with a public IP. You’re in charge:
- Install Python, Flask, Django, etc. yourself
- Run your own web servers and proxies
- Configure HTTPS certificates (Let’s Encrypt)
- Set up background jobs, databases, tunnels, whatever you like
So yes, a VPS can do everything PythonAnywhere does and far more — but you’re responsible for the setup and maintenance.
For your use case (test servers at home + wanting to avoid ngrok):
- With PythonAnywhere → you can only run code on their platform, not forward traffic to your home machine.
- With a VPS → you can forward traffic from the VPS to your home machine, or even just run the servers directly on the VPS if you prefer.
It’s like the difference between:
- PythonAnywhere: apartment in a serviced building (rules, but maintenance included).
- VPS: your own small house (do whatever you want, but upkeep is yours).
Running a VPN is one of the most common and useful things people do with a VPS.
Because the VPS has a public IP address, you can install a VPN server on it and then connect to it from your laptop, phone, or even your home network. That gives you:
- Secure browsing on public Wi-Fi (your traffic goes encrypted through the VPS instead of whatever café Wi-Fi you’re on).
- Bypass geo-restrictions (your traffic appears to come from wherever your VPS is located).
- Private tunnel into your home machine (exactly what you need to replace ngrok — you can connect your PC to the VPS through the VPN, then route incoming traffic back down the tunnel).
Popular choices:
- WireGuard (modern, fast, simple configs, my go-to recommendation)
- OpenVPN (older but widely supported)
So the picture looks like:
- VPS runs a VPN server (WireGuard or OpenVPN).
- Your PC at home connects to the VPN.
- The VPS reverse-proxies public requests down the VPN into your PC.
At that point the VPS is basically your personal gateway: web server, proxy, VPN, tunnel endpoint — whatever you need.
What ngrok is doing
- Your Windows machine opens a tunnel to ngrok.
- Someone connects to the ngrok public address → ngrok forwards the RDP traffic down that tunnel into port 3389 on your PC.
To do the same with your VPS
- Rent a VPS with a public IP.
- Create a tunnel from your PC → VPS (using SSH reverse tunnel or a VPN like WireGuard).
- Forward TCP port 3389 (the RDP port) across that tunnel.
- On the VPS, expose that port publicly, so you can RDP into the VPS IP, and the traffic goes back to your Windows machine.
VPN approach in WireGuard
- Set up WireGuard on the VPS (server) and on your Windows PC (client).
- When your PC connects, it gets a private VPN IP (say
10.0.0.2
). - Configure the VPS firewall to forward port 3389 on its public IP →
10.0.0.2:3389
. - Then you just RDP to the VPS public IP, and WireGuard hands it back to your PC.
Minimal roadmap (succinct)
Rent a small VPS (Ubuntu LTS).
Secure it: create a user, disable root SSH, enable firewall (allow 22, 80, 443, and WireGuard UDP port).
WireGuard server on VPS → Windows client at home.
- Result: your PC gets a private VPN IP (e.g., 10.0.0.2).
- You can RDP over the VPN and forward any dev ports privately.
Reverse proxy on VPS (Caddy or Nginx) to serve your domains.
Terminate HTTPS (Let’s Encrypt) on the VPS.
Proxy either to:
- Your home PC over WireGuard (10.0.0.2:port), or
- Apps running on the VPS (e.g., Gunicorn/Uvicorn).
DNS: point
A/AAAA
records at the VPS IP; proxy rules matchHost
or path.Migrate Flask from PythonAnywhere: same WSGI/ASGI app; run via
gunicorn/uvicorn
+systemd
; proxy from Caddy/Nginx.