Why a VPS
I have a lot of project ideas that need somewhere to live. Discord bots, web apps, game servers, APIs, databases — all of it currently either doesn't exist or is scattered across free tiers and my local machine. I wanted to consolidate onto something I control, that's always on, and that can grow with whatever I decide to build.
I also just wanted to get better at server administration. Running things locally behind a home router isn't the same as managing a real server with real security concerns. Time to learn properly.
The server
I went with OVHcloud. UK datacenter, good specs for the price, no traffic limits. The machine has 8 vCores, 24 GB of RAM, and 200 GB of NVMe storage — significantly more than I need right now, but I wanted headroom to run multiple things at once without them fighting for resources. Game servers in particular can be greedy.
OS is Ubuntu 24.04 LTS. Not a controversial choice. LTS means five years of security updates, great package support, and no surprises. I want to be bored by the operating system.
The stack
Rather than picking tools as I go and ending up with a mess, I decided upfront what the server's foundation would look like. Three things underpin everything:
Docker
Every service that can run in a container, will. Docker lets me isolate applications from each other and from the OS, manage dependencies without them conflicting, and tear things down and rebuild them cleanly. It also means moving workloads around later is straightforward — a Compose file and a volume backup is basically everything you need.
I installed Docker Engine from Docker's official repository rather than Ubuntu's package — Ubuntu's version lags significantly behind. Alongside it: Docker Compose (the modern plugin version, not the old standalone binary), Buildx for multi-platform builds if I ever need it.
One gotcha worth mentioning: Docker manipulates iptables directly to handle container port forwarding, which means it can bypass the host firewall if you're not careful. I set up a fix for this — containers that expose ports to the internet have to do it intentionally, not accidentally.
Caddy
Caddy is the reverse proxy that handles all public web traffic. When a request comes in for hipphamster.net or any subdomain, Caddy receives it and routes it to whatever service is running behind the scenes.
The reason I picked Caddy over Nginx or Apache is automatic HTTPS. You write a domain name in a config file, Caddy gets a Let's Encrypt certificate for it, handles renewal, and serves HTTPS — no manual cert management, no cron jobs, no forgetting to renew. This is how it should work and I'm glad someone built it this way.
# The whole config to serve a Docker app over HTTPS
app.hipphamster.net {
reverse_proxy localhost:8080
}
That's genuinely it. Caddy figures out the rest.
Tailscale
Not everything should be on the public internet. Databases, admin panels, internal dashboards — these are things I want to access from my own devices but that have no business being reachable by anyone else.
Tailscale creates a private network across all my devices. The VPS joins my Tailnet and gets a private IP that only my enrolled devices can reach. No open ports, no VPN configuration, no certificates to manage — it just works. When I eventually run SQL Server on this machine, it'll be accessible from my PC but invisible from the outside.
Security baseline
I'm not going to go into specifics here — that would defeat the point — but the general approach:
- SSH is key-only. Password authentication is disabled.
- Root login over SSH is disabled.
- UFW handles the host firewall. The only public ports are SSH, HTTP, and HTTPS.
- Fail2ban watches for repeated failed logins and bans offending IPs automatically.
- Unattended-upgrades runs daily security patches. Auto-reboot is off — I'll reboot intentionally after kernel updates.
None of this is exotic. It's just the standard stuff done properly. Most compromised servers got that way because someone skipped a step that took five minutes.
Directory structure
I set up a consistent directory layout under /srv/ before deploying anything:
/srv/
├── apps/ general applications
├── compose/ Docker Compose projects
├── databases/ database volumes and data
├── games/ game server files
├── websites/ static sites
└── backups/ local backup staging
The goal is that nothing ends up scattered across home directories. Every service has a predictable home. /srv/compose/myapp/ has the Compose file; /srv/databases/myapp/ has the data. Easy to find, easy to back up.
The domain
I pointed hipphamster.net at the server. It's not used for anything else so it made sense to give it to the VPS. When I deploy something worth showing, it gets a subdomain — app.hipphamster.net, mc.hipphamster.net, whatever makes sense. Adding a new subdomain is two steps: A record in the DNS panel, one new block in the Caddyfile.
What's next
The foundation is done. What goes on it is the interesting part. The short list includes a Minecraft server (modded, probably), SQL Server for local dev work accessible via Tailscale, and a few web projects that have been living on my local machine for too long.
The nice thing about having built this cleanly is that deploying something new is mostly just writing a Compose file. The hard infrastructure work is already done.