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

<Callout label="Alpha" variant="warn">
VPC tunnels use `apoxy alpha tunnel`. Pin the CLI version in automation and test upgrades before
rolling them into production.
</Callout>

The direct request path is:

```text
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](/docs/guides/custom-domains.md) if you still need one.
- Python 3 is available for the demo backend.

Check the active project before creating anything:

```bash title="terminal"
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:

```bash title="terminal"
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:

```bash title="terminal"
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

```yaml title="vpc-service.yaml"
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:

```bash title="terminal"
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.

```yaml title="domain.yaml"
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](/docs/guides/custom-domains.md).

Apply the DomainRecord before the HTTPRoute. Route admission rejects hostnames that are not yet
claimed by the project.

```bash title="terminal"
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:

```yaml title="route.yaml"
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
```

```bash title="terminal"
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:

```bash title="terminal"
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:

```js title="private-worker/index.js"
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(),
    });
  },
};
```

```yaml title="private-worker/service.yaml"
apiVersion: compute.apoxy.dev/v1alpha1
kind: Service
metadata:
  name: private-worker
```

Deploy it:

```bash title="terminal"
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:

```yaml title="worker-domain.yaml"
apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
spec:
  name: worker.example.com
  tls: {}
  target:
    ref:
      group: gateway.apoxy.dev
      kind: Gateway
      name: default
```

```yaml title="worker-route.yaml"
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:

```bash title="terminal"
apoxy apply -f worker-domain.yaml
apoxy apply -f worker-route.yaml
```

## Verify the compute route

```bash title="terminal"
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](/docs/guides/deploying-compute-services.md#outbound-traffic-egress) for policy controls.

<Callout type="warn">
**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](/docs/guides/ec2-vpc-tunnels.md). For Google Cloud, follow
[GCE userspace VPC tunnels](/docs/guides/gce-vpc-tunnels.md).
</Callout>

## Clean up

Stop the local tunnel and backend with Ctrl-C, then remove the resources you applied:

```bash title="terminal"
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
```

---

**Navigation** (Guides)

- Previous: [Tunnels with Docker](/docs/guides/tunnels-with-docker.md)
- Next: [GCE userspace VPC tunnels](/docs/guides/gce-vpc-tunnels.md)
- All pages: [index](/docs/llms.txt)
