Self-hosting services on a custom domain securely
September 23, 2025
My Setup
- Proxmox: Running on a bare metal server.
- Basic Linux VM: Running services I would like to access securely.
- Custom domain: I have the domain
alexboden.cawith DNS records managed by Cloudflare.
The Goal
- Private by default: Services should only be reachable from my devices.
- Single URL per service: e.g.
git.alexboden.ca,jellyfin.alexboden.ca,proxmox.alexboden.ca. - Clean HTTPS: No browser warnings, even on phones.
- No public exposure: No NAT forwards, no dangling ports.
The Solution
I combined Tailscale Serve with Caddy running inside a VM as a single TLS termination point:
- Tailscale Serve: Accepts incoming HTTPS traffic on port 443, does raw TCP passthrough.
- Caddy in a VM: Terminates TLS using a private CA, then reverse-proxies each service.
- Proxmox host: Accessed over its own Tailscale IP.
The Setup
Tailscale:
You can download tailscale here.
sudo tailscale serve reset
sudo tailscale serve --bg --tcp=443 tcp://localhost:443
This allows incoming traffic on port 443, the standard port for HTTPS.
Caddy + Cloudflare
You can download Caddy here. Since I manage my DNS with Cloudflare, I also installed the Cloudflare DNS module.
I installed Caddy as a systemd service on my self hosted VM. I added the following to my Caddyfile (/etc/caddy/Caddyfile):
(cloudflare) {
tls {
dns cloudflare CLOUDFLARE_API_TOKEN
}
}
# Example of a reverse proxy for a service running on the same VM as Caddy
git.alexboden.ca {
reverse_proxy http://localhost:3000
import cloudflare
}
# Example of a reverse proxy for a service in the same tailnet (with a different )
proxmox.alexboden.ca {
reverse_proxy https://100.109.111.101:8006 {
transport http {
tls_insecure_skip_verify
}
header_up Host {host}
header_up X-Forwarded-Proto https
header_up X-Forwarded-For {remote}
}
import cloudflare
}
Then reload Caddy:
systemctl restart caddy
Add the following DNS records to Cloudflare:
| Type | Name | Content | Proxy Status | TTL |
|---|---|---|---|---|
| CNAME | git.alexboden.ca | vm-name.tailnet-name.ts.net | DNS only | Auto |
| CNAME | proxmox.alexboden.ca | vm-name.tailnet-name.ts.net | DNS only | Auto |
You can find the VM name and tailnet name in the Tailscale dashboard.
Diagram of the setup
flowchart TD
subgraph Cloudflare[Cloudflare DNS]
DNS1["git.alexboden.ca"]
DNS2["proxmox.alexboden.ca"]
end
subgraph Tailnet[Tailscale Network]
U["User Devices\n(Laptop, Phone)"]
TS["Tailscale Serve (TCP:443 passthrough)"]
VM["Caddy (TLS Termination + Reverse Proxy)"]
PVE["Proxmox Host (Bare Metal, Tailscale IP)"]
Gitea["Gitea Service localhost:3000"]
end
%% Flows
U -->|HTTPS Request git.alexboden.ca| DNS1
U -->|HTTPS Request proxmox.alexboden.ca| DNS2
DNS1 --> TS
DNS2 --> TS
TS --> VM
VM -->|Local Reverse Proxy| Gitea
VM -->|Reverse Proxy via Tailscale IP| PVE