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.

SScottJan 03, 2026 · 12 min readOriginal case study
Architecture diagram for a Terraform-managed Xray Reality VPN on GCP and VPS nodes.
GitHub PRs drive preview, lint, apply, fleet reconciliation, secrets, VPS rollout, and short-lived logging into BigQuery.

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.

ProvisioningTerraform + GCP Infrastructure Manager
CI/CDGitHub PRs, Cloud Build preview, lint, and apply triggers
RuntimeTwo VPS nodes, Tokyo and Singapore, running Xray
SecretsGCP Secret Manager for credentials and protocol material
ProtocolVLESS + Reality over 443, shaped like ordinary TLS
LogsCloud 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 links

Why 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.
Design note

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.

Happ client connected through a Singapore VPN endpoint with an IP lookup in the background.
Happ connected to Singapore, with the browser showing the exit IP geolocated there.
Shadowrocket showing Tokyo and Singapore local VLESS servers.
Shadowrocket exposes the same fleet as ordinary selectable routes.
Globalping connectivity test against the VPN endpoint from probes in China.
External probes are useful because they test the public path from outside my laptop, not just whether my client app says “connected.”

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.