Personal VPS Fundamentals
This series follows the VPS from a safe baseline to its first real shadow workload: verify the machine, preserve remote access, close public SSH, add shared ingress and private monitoring, then prove the application path with RSSHub. The new instance is usable, but an existing feed consumer still uses the local RSSHub; no production consumer has been cut over.
If you are still choosing where to run an application, compare this self-managed server approach with Cloudflare Workers and Google Cloud's Cloud Run tutorial. Those guides cover managed execution, permissions, storage, and billing; this series focuses on operating the Linux host itself.
Start with the big picture: two doors
Think of the VPS as a machine room placed on the Internet. It now has two very different ways in:
- The management door is for me: WSL2 first takes the private Tailscale route, then OpenSSH checks my key and opens a terminal.
- The website door is for Internet visitors: every public HTTP/HTTPS request reaches Caddy first, and Caddy decides which website or API should receive it.
Uptime Kuma is an administration tool, not a public website. It has no public door; I create a temporary SSH tunnel when I need to view it.
[Me: WSL2]
`-- private Tailscale road
`-- locked OpenSSH management door
`-- [Ubuntu VPS]
[Internet visitor]
`-- TCP 80/443
`-- Caddy public reception desk
|-- infrastructure health check
`-- infra-edge corridor -> RSSHub:1200
[Inside the VPS]
|-- Docker runs Caddy, Uptime Kuma, and RSSHub separately
|-- UFW controls which host ports may receive traffic
|-- Uptime Kuma waits on 127.0.0.1:3001 for an SSH tunnel
`-- RSSHub port 1200 exists only inside the Docker network
[Existing local environment]
`-- feed consumer -> local RSSHub (production path and fallback)
Three layers matter: network reachability, the OpenSSH protocol and host identity, and user public-key authentication. Tailscale supplies the private network path here; Ubuntu OpenSSH still listens on port 22 and authenticates the user. The separate Tailscale SSH product is not enabled.
Vocabulary
Installed or enabled tools
The components below were actually installed, enabled, or explicitly configured during Phases 1–3. Start with the analogy, then attach the technical name to it.
Tools that let me sign in safely
Tailscale and OpenSSH are different: Tailscale builds the road to the server; OpenSSH checks the key when I reach the door.
Tools that run services
The three current instruction files are /srv/caddy/compose.yaml, /srv/uptime-kuma/compose.yaml, and /srv/rsshub/compose.yaml. They are independent, so restarting RSSHub does not restart ingress or monitoring.
Tools for public ingress and monitoring
Caddy and RSSHub join the Docker network named infra-edge. Think of it as an internal corridor from reception to the Web workload: Caddy resolves the alias rsshub-prod, while RSSHub opens no host or public port. Kuma stays outside this proxy path and remains available only through its SSH tunnel.
When will I use each one?
- Signing in to the VPS: connect Tailscale, then run
ssh; Tailscale, UFW, and OpenSSH work together behind the scenes. - Starting, stopping, or inspecting a service: use
docker compose; do not edit Docker's internal files directly. - Opening Kuma: create the SSH local forward, then browse to
http://127.0.0.1:3001; do not make port 3001 public. - Checking the public endpoint: open the HTTPS
/healthzURL served by Caddy. - Using RSSHub: a trusted consumer calls the HTTPS domain with the ACCESS_KEY; the key moves only from the protected VPS env file to trusted consumers, never into public notes or monitoring URLs.
- Buildx: use it only when building a custom image; routine operation rarely needs it.
Three boundaries to remember
- Only public TCP 80/443 enters, and it reaches Caddy first.
- SSH travels only through the Tailscale private network; public SSH stays blocked.
- Kuma listens only on the VPS's own
127.0.0.1:3001and is viewed through an SSH tunnel. - RSSHub port 1200 is not bound to the host; only Caddy can reach it through
infra-edge.
“UFW active” alone does not prove that Docker ports are safe because Docker has its own port-forwarding rules. For every new service, check the Compose ports, Docker's actual bindings, and a connection from another machine. The service-addition guide has the commands, and the Build Log has the deployment timeline.
Reading path
- Architecture and safety
- SSH access and hardening
- Tailscale networking and OpenSSH
- UFW without lockout
- Docker Engine and Compose
- Operations runbook
- Practical Tmux
- Adding services beside an existing bot
- RSSHub production shadow deployment
Safety invariants
- Label every command as local WSL2 or remote VPS.
- Never remove the last verified SSH path.
- Test changes with a fresh connection; an old session can survive a broken firewall rule.
- Never publish passwords, private keys, tokens, recovery codes, real addresses, or fingerprints.
Examples use placeholders:
VPS_USER=ubuntu
VPS_PUBLIC_IPV4=<public-ip>
TAILSCALE_IPV4=<100.x.x.x>
LOCAL_SSH_PUBLIC_KEY="$HOME/.ssh/id_ed25519.pub"
After Phases 1–3, the VPS has a patched Ubuntu host, key-only OpenSSH over Tailscale, public TCP 80/443 through Caddy, Docker with Buildx and Compose, private Kuma monitoring, and a digest-pinned RSSHub shadow workload with memory cache, ACCESS_KEY, and no host port. Public health, unauthenticated denial, an authenticated real route, monitoring, and application restart have all been verified.
The feed consumer's production configuration, persistent data, scheduler, and local RSSHub remain unchanged. Consumer cutover, backup and restore proof, scheduler migration, and the decision to retire the local fallback belong to Phase 4.