Getting startedGuidesReferenceChangelog
Apoxy:// Docs / Guides / Private VPC services

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.

Alpha

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:

$terminalTXT
HTTPS → default Gateway → HTTPRoute → Backend → VPCService → tunnel → private service

Check 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:

$terminalSH
apoxy auth --check apoxy gateway get default apoxy vpc network get default apoxy vpc relay list

Publish a private service

Start the backend

Run a small HTTP server on the private machine:

$terminalSH
mkdir -p private-api printf '%s\n' 'Hello through an Apoxy VPC tunnel.' > private-api/index.html python3 -m http.server 8080 --directory private-api

Leave it running.

Connect the tunnel

In another terminal, connect the machine to the default VPC network:

$terminalSH
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

$vpc-service.yamlYAML
apiVersion: vpc.apoxy.dev/v1alpha1 kind: VPCService metadata: name: private-api spec: networkRef: name: default selector: matchLabels: tunnel.apoxy.dev/name: private-api

Apply it and confirm that it has at least one endpoint:

$terminalSH
apoxy apply -f vpc-service.yaml apoxy vpc service get private-api -o yaml

Continue 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.

$domain.yamlYAML
apiVersion: core.apoxy.dev/v1alpha3 kind: DomainRecord spec: name: api.example.com tls: {} target: ref: group: gateway.apoxy.dev kind: Gateway name: default

For 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.

$terminalSH
apoxy apply -f domain.yaml

Route 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:

$route.yamlYAML
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: 8080
$terminalSH
apoxy apply -f route.yaml

VPC-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:

$terminalSH
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:

$private-worker/index.jsJS
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(), }); }, };
$private-worker/service.yamlYAML
apiVersion: compute.apoxy.dev/v1alpha1 kind: Service metadata: name: private-worker

Deploy it:

$terminalSH
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:

$worker-domain.yamlYAML
apiVersion: core.apoxy.dev/v1alpha3 kind: DomainRecord spec: name: worker.example.com tls: {} target: ref: group: gateway.apoxy.dev kind: Gateway name: default
$worker-route.yamlYAML
apiVersion: 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-worker

Apply the domain first, then the route:

$terminalSH
apoxy apply -f worker-domain.yaml apoxy apply -f worker-route.yaml

Verify the compute route

$terminalSH
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.

Heads up

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:

$terminalSH
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