Skip to main content

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:

  1. The management door is for me: WSL2 first takes the private Tailscale route, then OpenSSH checks my key and opens a terminal.
  2. 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​

TermMeaning here
VPSA virtual Linux machine supplied by a cloud provider
Public IPAn address routable from the Internet
Tailscale IPA private 100.x.x.x address inside a tailnet
PortA network service entry point, such as TCP 22 for SSH
daemonA long-running service such as sshd, tailscaled, or dockerd
UFWUbuntu's host-firewall frontend
image / containerA read-only template / a running instance of that template
ComposeA Docker plugin describing applications in YAML

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​

ToolThink of it asProblem it solves
OpenSSH ServerA locked management door that accepts the right keyConfirms that the client owns the private key matching a registered public key, then provides a remote command line; the private key is never sent to the server
TailscaleA private road connecting only my devicesLets WSL2 find the VPS through a private address instead of exposing management to the whole Internet
UFWThe visitor list at the gateDenies unsolicited inbound connections by default and permits only public Web ports plus SSH arriving through the Tailscale interface

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​

ToolThink of it asProblem it solves
Docker EngineA building manager for isolated roomsRuns each service in a separate container and manages images, networks, and persistent mounts
Docker ComposeThe furnishing and startup instructions for each roomRecords the image, data path, ports, and restart policy in one YAML file
Docker BuildxA workbench for making new room templatesBuilds custom Docker images when an existing image is not enough; normal Caddy and Kuma operation does not require it

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​

ToolThink of it asProblem it solves
CaddyThe public reception desk for websitesReceives all public traffic on 80/443, manages HTTPS certificates and redirects, then sends each request to the right service
Uptime KumaThe watchkeeper and status boardRepeatedly checks health URLs and records whether services are online; its management screen remains private
RSSHubA translation desk that turns site updates into feedsRuns real RSS routes and uses ACCESS_KEY to stop strangers from consuming the private instance

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 /healthz URL 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:3001 and 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​

  1. Architecture and safety
  2. SSH access and hardening
  3. Tailscale networking and OpenSSH
  4. UFW without lockout
  5. Docker Engine and Compose
  6. Operations runbook
  7. Practical Tmux
  8. Adding services beside an existing bot
  9. 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.

Explore connectionsOpen network