Publishing gives something that runs on this machine a real address on the web: https://<name>.<your-domain>, HTTPS on a domain of your own. A web app in a VM or a service on the host becomes a link you can open from anywhere and hand to someone else.
exe expose command or one line over SSH, and hands you the address. The built-in Chat agent has the same as a tool.This chapter puts a running service on the web. Publishing to GitHub puts a project's code on GitHub, and Analytics shows who came to what you published.
A published name is a route: a hostname, and where its requests are sent. Publishing one does three things:
Routes survive a daemon restart.
The proxy gzips text a backend sends uncompressed, such as HTML, CSS, scripts, JSON and SVG, so a page goes up the tunnel at about a quarter of its size. Images, video, byte ranges and event streams pass through as they are.
Publishing needs three things at Cloudflare:
Then choose Special → Cloudflare Setup Wizard…, or Set Up… on the Cloudflare tab of Windows → Configuration, and give them to exe:
<name>.<this domain>: use the zone's own name (example.com) or a namespace under it, such as apps.example.com.The wizard writes the cloudflare.* settings (api_token, account_id, zone_id, tunnel_id, domain and metrics_url). The wizard alone sets them, because they must belong together: the account, the zone and the tunnel, and a token that may edit that zone. Configuration → Cloudflare shows them, with the account, the zone and the tunnel by name, and whether the last check passed. To change one, click Change… there. The wizard opens with every answer filled in, and you change only the one you want.
Two things decide whether a published name finds its way back to this machine:
advertise_host, in Windows → Configuration, is this machine's address as the cloudflared host reaches it: a LAN or Tailscale IP. A new install fills in the Tailscale IP when the machine has one. The wizard's last step warns when it is empty; a publish then skips the tunnel rule and says so. When you change it, Save repoints the tunnel rule of every published name.0.0.0.0. One that listens on 127.0.0.1 only cannot be published.The tab prints the route, the tunnel rule and the address, https://<sub>.<your-domain>, with a warning line for any step that failed. After a failed Cloudflare call the wizard opens, so you can check the settings.
The Services tab is a shortcut: it lists the ports listening in the VM, and Expose beside one publishes it under the VM's name.
A name leads to one place. Publishing under a name that is already in use moves it to the new port, so a VM's second port needs a subdomain of its own.
exe routes prints the same list in a terminal.Unpublishing removes the DNS record, the tunnel rule and the route. What was published is not touched: an app keeps running in its VM and can be exposed again.
exe expose demo -port 8000 -sub guestbook # https://guestbook.example.com exe routes # every name and where it leads exe unexpose guestbook.example.com # remove one, by its full name
-sub is optional: without it the name is the VM's. The SSH lobby (SSH from your own terminal) has the same three commands, for a script or an agent on another machine:
ssh -p 2222 exe@this-host expose demo 8000 guestbook ssh -p 2222 exe@this-host routes --json ssh -p 2222 exe@this-host unexpose guestbook.example.com
An agent that drives exe over HTTP has the whole of publishing:
| To | Call |
|---|---|
| See what listens in a VM | GET /v1/vms/{name}/ports |
| Publish a VM port | POST /v1/vms/{name}/expose with port and, optionally, subdomain |
| Publish a redirect | POST /v1/routes/redirect with host and target |
| Publish a service that is not in a VM | POST /v1/routes with host and backend |
| List the routes | GET /v1/routes |
| Unpublish a name | DELETE /v1/routes/{host} |
A publish answers with host, url and backend. A warnings array in the answer means it only partly applied. To publish a VM port, the VM must be running and Cloudflare set up.
Help → Agent Skill Guide… opens the guide exe serves to agents at /skill.md; it teaches all of these calls but POST /v1/routes. The built-in Chat agent (The coding agent) has tools for a VM's ports: it lists ports and routes and publishes on its own, and unpublishes only after you approve a confirmation dialog.
A name can answer with a permanent redirect instead, with no VM behind it:
exe expose example.com -redirect https://exe.example.com exe expose www.example.com -redirect https://exe.example.com
https://example.com/docs/?from=home becomes https://exe.example.com/docs/?from=home. The method and body survive when the client follows the redirect.exe routes lists these routes as redirect:https://…, and exe unexpose <host> removes one.A hostname can also route to a service on this machine that is not a VM, such as a daemon on the loopback address:
exe expose charts.example.com -backend http://127.0.0.1:7799
Host, so one backend can serve several names.exe unexpose <host> removes them.A hostname can be a folder on this machine, served by the daemon itself, so a static site needs no server process of its own:
exe expose site.example.com -dir /www/site
/ is the folder's index.html, /pictures/a.png the file of that name; a folder with no index.html is not listed, and a file or folder whose name begins with a dot is never served..dmg, .img, .toast, .iso, .sit, .zip, .bin, .hqx) is sent as a download rather than shown..password is served only to a request carrying HTTP Basic authentication whose password is that file's first line, under any user name; everything beneath the folder is behind it, and the browser asks once. Those answers are marked private and never stored, so the edge in front of the site cannot hand them on.exe routes lists it as exe:dir:/www/site; exe unexpose <host> removes it. The folder is read at each request, so a new file is live at once. Pages are served no-cache; other files may be kept for ten minutes.POST /v1/routes with {"host": "site.example.com", "dir": "/www/site"}.-redirect, -backend and -dir take a full hostname and stand alone: none combines with -port, -sub or another of the three.
The daemon carries a site of its own inside the binary: the exe homepage and its documentation. exe site publishes it under your domain, with the same DNS record and tunnel rule as a VM port:
exe site # https://exe.example.com exe site -sub home # https://home.example.com
exe routes shows it as exe:site, and exe unexpose <host> removes it like any other name.
A route can also say where the same site lives on IPFS, through a DNSLink record:
PUT /v1/routes/<host>/dnslink with {"path": "/ipfs/<cid>"} writes it: the TXT record _dnslink.<host> holding dnslink=/ipfs/<cid>. ipfs resolve /ipns/<host> and IPFS gateways then find that content.DELETE on the same path removes the record, and so does exe unexpose <host>./ipfs/<cid> or /ipns/<name>.exe-planet, the site builder that runs beside exe, does this for an exposed site that also publishes to IPFS: every publish writes its new CID there.
Live local traffic is read from cloudflared's loopback metrics listener at http://127.0.0.1:20241. If it uses another local port, set metrics_url on the wizard's last step to the base address (without /metrics). exe verifies that the local connector belongs to the selected tunnel.
A machine without a local cloudflared connector can still show the tunnel-wide connection counts; its local counters stay unavailable. A Cloudflare API failure labels cached connection counts as last known rather than reporting them as live.
This one is not publishing. It lets the desktop itself reach a service on this machine, with the API token still on guard.
A daemon on this machine's loopback, such as 127.0.0.1:7799, is out of a browser's reach when the desk is opened over Tailscale Serve or from a phone. Name it in ~/.exe/config.json and the daemon relays to it:
"services": { "planet": "http://127.0.0.1:7799" }
/v1/svc/planet/<path> then reaches http://127.0.0.1:7799/<path> with the method, body and query unchanged.?token=; nor are the desk's cookies. The service's cookies and CORS headers stay behind.services through PUT /v1/config takes effect at once.