Private services with VPC tunnels
Publish a private service through the default Gateway, or call it from an Apoxy compute Service.
This guide connects a private HTTP service to Apoxy's VPC overlay, publishes it through the default Gateway, and then calls the same service from an Apoxy compute Service.
VPC tunnels use apoxy alpha tunnel. Pin the CLI version in automation and test upgrades before
rolling them into production.
The direct request path is:
HTTPS → default Gateway → HTTPRoute → Backend → VPCService → tunnel → private serviceCheck prerequisites
- The Apoxy CLI is installed and authenticated.
- The managed Apoxy project is ready.
- You have a domain in Apoxy. See Custom domains if you still need one.
- Python 3 is available for the demo backend.
Check the active project before creating anything:
apoxy auth --check
apoxy gateway get default
apoxy vpc network get default
apoxy vpc relay listPublish a private service
Start the backend
Run a small HTTP server on the private machine:
mkdir -p private-api
printf '%s\n' 'Hello through an Apoxy VPC tunnel.' > private-api/index.html
python3 -m http.server 8080 --directory private-apiLeave it running.
Connect the tunnel
In another terminal, connect the machine to the default VPC network:
apoxy alpha tunnel run \
--name private-api \
--vpc default \
--admin-addr localhost:8081 \
--socks-addr ""The admin listener is disabled by default. This command enables it on port 8081, so the backend
can keep port 8080. Use /livez for process health, /readyz for connection readiness, and
/metrics for Prometheus metrics. VPC peers cannot reach the admin listener through the tunnel.
The agent registers the label tunnel.apoxy.dev/name=private-api, which the VPCService below
selects.
Select the tunnel with a VPCService
apiVersion: vpc.apoxy.dev/v1alpha1
kind: VPCService
metadata:
name: private-api
spec:
networkRef:
name: default
selector:
matchLabels:
tunnel.apoxy.dev/name: private-apiApply it and confirm that it has at least one endpoint:
apoxy apply -f vpc-service.yaml
apoxy vpc service get private-api -o yamlContinue when the Ready condition is True and status.endpoints is not empty.
Attach the hostname to the default Gateway
Replace api.example.com with your hostname. Omit metadata.name; Apoxy generates it from the
hostname and target type.
apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
spec:
name: api.example.com
tls: {}
target:
ref:
group: gateway.apoxy.dev
kind: Gateway
name: defaultFor a hostname below an Apoxy-managed zone, also set spec.zone to that zone's root. For a
customer-owned hostname, publish the CNAME described in Custom domains.
Apply the DomainRecord before the HTTPRoute. Route admission rejects hostnames that are not yet claimed by the project.
apoxy apply -f domain.yamlRoute to the VPCService
The VPCService is available inside Apoxy at
private-api.default.vpc.apoxy.net. Wrap that name in a Backend, then reference it from an
HTTPRoute:
apiVersion: core.apoxy.dev/v1alpha2
kind: Backend
metadata:
name: private-api
spec:
endpoints:
- fqdn: private-api.default.vpc.apoxy.net
---
apiVersion: gateway.apoxy.dev/v1
kind: HTTPRoute
metadata:
name: private-api
spec:
parentRefs:
- name: default
port: 443
hostnames:
- api.example.com
rules:
- backendRefs:
- group: core.apoxy.dev
kind: Backend
name: private-api
port: 8080apoxy apply -f route.yamlVPC-backed Backends are not limited to the default Gateway. An HTTPRoute on a dedicated
Gateway (one bound to its own Proxy through infrastructure.parametersRef) reaches the
VPCService the same way; only the parentRefs entry changes.
When a VPCService selects agents in more than one location, requests automatically prefer the lowest-latency healthy path and spill over to the others on failure.
Verify the direct route
Check certificate status, then send a public request:
apoxy domain list \
--field-selector spec.name=api.example.com \
-o yaml
curl https://api.example.com/The first certificate normally takes 30-60 seconds. A successful request returns
Hello through an Apoxy VPC tunnel.
Call the VPCService from a compute Service
Compute Services use the same project-scoped VPC name with ordinary fetch(). There is no
VPCService binding to add to the Service manifest.
Create a worker in a new directory:
const upstream = "http://private-api.default.vpc.apoxy.net:8080/";
export default {
async fetch() {
const response = await fetch(upstream);
return Response.json({
upstream,
status: response.status,
body: await response.text(),
});
},
};apiVersion: compute.apoxy.dev/v1alpha1
kind: Service
metadata:
name: private-workerDeploy it:
cd private-worker
apoxy auth --check
apoxy deploy . --yes
apoxy compute service get private-worker -o yaml
cd ..The CLI requires --yes for a production project. Always inspect the active project immediately
before that command; omit the flag when you want the safety check to stop a mistaken deployment.
Create a second DomainRecord and route for the worker:
apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
spec:
name: worker.example.com
tls: {}
target:
ref:
group: gateway.apoxy.dev
kind: Gateway
name: defaultapiVersion: gateway.apoxy.dev/v1
kind: HTTPRoute
metadata:
name: private-worker
spec:
parentRefs:
- name: default
port: 443
hostnames:
- worker.example.com
rules:
- backendRefs:
- group: compute.apoxy.dev
kind: Service
name: private-workerApply the domain first, then the route:
apoxy apply -f worker-domain.yaml
apoxy apply -f worker-route.yamlVerify the compute route
apoxy domain list \
--field-selector spec.name=worker.example.com \
-o yaml
curl https://worker.example.com/The response includes the VPC upstream, its 200 status, and the private backend body. Compute
egress uses the project's default EgressGateway unless the Service selects another one; see
Outbound traffic for policy controls.
Cloud Run is not yet supported. Default Cloud Run egress drops the agent's 1,280-byte QUIC
Initial above its measured 1,252-byte UDP payload limit. Direct VPC egress reaches the relay, but
the current userspace data plane then fails underlay address-family selection. Kernel mode also
needs NET_ADMIN and /dev/net/tun, which Cloud Run does not expose. Use a VM or Kubernetes. For
AWS, follow EC2 kernel-mode VPC tunnels. For Google Cloud, follow
GCE userspace VPC tunnels.
Clean up
Stop the local tunnel and backend with Ctrl-C, then remove the resources you applied:
apoxy delete -f worker-route.yaml
apoxy delete -f worker-domain.yaml
apoxy delete -f private-worker/service.yaml
apoxy delete -f route.yaml
apoxy delete -f domain.yaml
apoxy delete -f vpc-service.yaml