1---
2title: Expose private origins with Tunnel
3description: Connect a private origin to Cloudflare's edge with a Tunnel, declare its ingress rules as a Configuration resource, and point DNS at it with a proxied CNAME.
4sidebar:
5 order: 25
6---
7
8A Cloudflare Tunnel establishes an outbound connection from your
9origin — a home lab, a Kubernetes cluster, a VM with no public IP —
10to Cloudflare's edge. No open inbound ports, no public address. In
11alchemy the tunnel, its routing rules, and the DNS record that
12publishes it are all ordinary resources in one stack.
13
14## Create a tunnel
15
16```typescript
17import * as Cloudflare from "alchemy/Cloudflare";
18
19const tunnel = yield* Cloudflare.Tunnel.Tunnel("MyTunnel");
20// Run the connector on your origin:
21// cloudflared tunnel run --token <Redacted.value(tunnel.token)>
22```
23
24The resource creates a remotely-managed `cfd_tunnel` and exposes
25`tunnelId` plus a redacted connector `token` — hand the token to
26`cloudflared` on the origin and it dials out to the edge. If you
27omit `name`, a unique one is generated from the app, stage, and
28logical ID; tunnel names are immutable, so changing the name (or
29the `tunnelSecret`) triggers a replacement.
30
31## Route hostnames with a Configuration
32
33A remotely-managed tunnel has a single routing document: an ordered
34list of ingress rules that `cloudflared` matches top-down, first
35match wins. The `Configuration` resource owns that document:
36
37```typescript
38yield* Cloudflare.Tunnel.Configuration("Ingress", {
39 tunnelId: tunnel.tunnelId,
40 ingress: [
41 { hostname: "ui.internal", service: "http://ui.app.svc.cluster.local:80" },
42 { hostname: "api.internal", service: "http://api.app.svc.cluster.local:8080" },
43 ],
44});
45```
46
47`service` is the upstream each hostname forwards to — a private IP,
48a Kubernetes Service DNS name, or a pseudo-service like
49`http_status:404`. The required catch-all rule (a final entry with
50no hostname) is appended automatically, since Cloudflare rejects a
51config whose last rule has a hostname; override what it returns
52with `catchAllService: "http_status:503"`.
53
54:::note
55For simple cases you can skip the separate resource and pass
56`ingress` directly to the `Tunnel` itself — there, you author the
57catch-all rule yourself (end with `{ service: "http_status:404" }`).
58Both apply only to remotely-managed tunnels (`configSrc:
59"cloudflare"`, the default); a `"local"` tunnel is configured by a
60YAML file on the origin instead.
61:::
62
63## Point DNS at the tunnel
64
65Publishing a hostname through the tunnel is a proxied CNAME to
66`<tunnelId>.cfargotunnel.com`:
67
68```typescript
69yield* Cloudflare.DNS.Record("AppCname", {
70 zoneId: zone.zoneId,
71 name: "app.example.com",
72 type: "CNAME",
73 content: `${tunnel.tunnelId}.cfargotunnel.com`,
74 proxied: true,
75});
76```
77
78`proxied: true` is what makes it work — traffic must enter
79Cloudflare's edge to be forwarded down the tunnel. See
80[Domains & DNS](/cloudflare/networking/domains) for creating or adopting the
81`zone` this record lives in.
82
83## Private hostnames and networks
84
85For origins that should only be reachable by WARP clients — no
86public DNS at all — route the hostname through the tunnel with a
87`HostnameRoute`:
88
89```typescript
90yield* Cloudflare.Tunnel.HostnameRoute("AppRoute", {
91 hostname: "app.internal.example.com",
92 tunnelId: tunnel.tunnelId,
93 comment: "internal wiki behind the datacenter tunnel",
94});
95```
96
97Hostname, tunnel, and comment are all mutable in place — updates
98converge on the same route rather than replacing it. Hostnames are
99unique per account.
100
101To route entire private CIDR ranges — including overlapping ones —
102create a `VirtualNetwork` and target it from a `Route`:
103
104```typescript
105const vnet = yield* Cloudflare.Tunnel.VirtualNetwork("Staging", {
106 comment: "staging private network",
107});
108
109yield* Cloudflare.Tunnel.Route("StagingNet", {
110 tunnelId: tunnel.tunnelId,
111 network: "10.4.0.0/16",
112 virtualNetworkId: vnet.virtualNetworkId,
113});
114```
115
116Each virtual network is an isolated routing namespace, so two
117environments can both claim `10.4.0.0/16` and WARP clients pick
118which copy they see. Passing `vnet.virtualNetworkId` into the
119`Route` also gets destroy ordering right — a virtual network can't
120be deleted while routes still reference it.
121
122## Where next
123
124- [Domains & DNS](/cloudflare/networking/domains) — the zone and records the
125 tunnel CNAME lives in.
126- [Tunnel API reference](/providers/cloudflare/tunnel/tunnel) —
127 including the `ReadTunnel` / `WriteTunnel` / `ReadWriteTunnel`
128 bindings for managing tunnels from inside a deployed Worker.
129- [Configuration API reference](/providers/cloudflare/tunnel/configuration)
130- [HostnameRoute API reference](/providers/cloudflare/tunnel/hostnameroute)
131- [VirtualNetwork API reference](/providers/cloudflare/tunnel/virtualnetwork)