Building a custom VPN with Xray VLESS + Reality
A personal systems project: a stealthier VPN fleet, managed with Terraform, deployed through Cloud Build, backed by Secret Manager, and observed just enough to debug without turning logs into a diary.

Why I built it
I wanted a VPN I could actually understand. Not a black-box app with a nice toggle, not a fragile shell script on a random server, and not a dashboard full of long-lived secrets. The goal was a small system I could rebuild, rotate, monitor, and explain from end to end.
The threat model was practical rather than theatrical: keep traffic off the public default path, avoid obviously VPN-shaped handshakes, make server rotation boring, and avoid collecting more information about my own browsing than I needed to operate the service.
System shape
The system has two halves. GCP owns the control plane: repository events, preview/apply workflows, secret storage, and logging sinks. The VPS fleet owns the data plane: regional Xray instances that clients connect to over 443.
| Provisioning | Terraform + GCP Infrastructure Manager |
|---|---|
| CI/CD | GitHub PRs, Cloud Build preview, lint, and apply triggers |
| Runtime | Two VPS nodes, Tokyo and Singapore, running Xray |
| Secrets | GCP Secret Manager for credentials and protocol material |
| Protocol | VLESS + Reality over 443, shaped like ordinary TLS |
| Logs | Cloud Logging sink to BigQuery, error-level only, short retention |
That split keeps the disposable pieces disposable. If a VPS needs to move, the desired state is still in Terraform and the credentials are still outside the repository. If a key needs rotation, the pipeline can regenerate material and push a fresh client link.
Terraform is the map, but not the whole journey
Terraform gives the project a real map: projects, service accounts, IAM, Cloud Build triggers, Secret Manager entries, log sinks, and deployment wiring are all expressed as infrastructure. But the VPS reality still needs a bootstrap phase. A server has to exist before it can run an agent, receive Xray config, and join the fleet.
# Conceptual flow, not a secret-bearing bootstrap script
terraform plan
terraform apply
cloud-build: fleet-reconcile
scp xray config + agent creds
systemctl restart xray
publish fresh VLESS linksWhy VLESS + Reality
I chose Xray with VLESS and Reality because the protocol can blend into ordinary TLS-looking traffic. From the outside, the handshake should look like a browser talking to a normal site rather than a bright neon sign that says “VPN tunnel here.”
The best diagram for this system is not “client → VPN → internet.” It is “GitHub → GCP control plane → regional VPS fleet → client profiles,” because the operating lifecycle matters as much as the tunnel.
That does not make the system magical. It still needs boring hygiene: patched hosts, a minimal firewall, non-root services, low-noise logs, and a way to rotate configuration without manually editing servers at 1 a.m.
Client setup and verification
The client experience is intentionally dull: paste a VLESS link, choose Tokyo or Singapore, connect, and verify the route from a plain IP lookup or an external probe. The less bespoke the client workflow feels, the easier the system is to keep usable.



Logging without building a diary
I wanted enough visibility to debug broken deployments, dead services, or unhealthy hosts. I did not want a detailed record of my own traffic. So the logs stay intentionally narrow: host metrics, Xray errors, Cloud Build events, and a short-retention sink into BigQuery.
- VPS logs flow through Cloud Logging and into BigQuery.
- Access logs stay off; noisy debug/info logs are filtered out.
- Tables expire after a week, which keeps both cost and risk small.
What I would reuse
The reusable part is not the exact VPN stack. It is the habit: draw the control plane, keep secrets out of code, automate the boring reconciliation step, make validation external, and decide up front what telemetry you are willing to keep.