Publishing to the web
Text

Publishing to the web

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.

  • A link to share. One click in a VM's window publishes a port, and the service is live at its address.
  • Nothing to open, nothing to renew. Visitors come in through your Cloudflare Tunnel, so no port is forwarded on your router, and Cloudflare answers HTTPS for the name: there is no certificate to manage.
  • Undone as quickly. Unpublishing removes the address. The app keeps running in its VM and can be published again.
  • An agent can finish the job. A coding agent that built something in a VM publishes it with one HTTP call, one 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.

What publishing does

A published name is a route: a hostname, and where its requests are sent. Publishing one does three things:

  1. Creates the hostname's DNS record at Cloudflare, pointing at your tunnel.
  2. Adds a rule to the tunnel, its ingress, that sends the hostname to exe's reverse proxy on this machine.
  3. Adds the route to that proxy, which passes each request on to the VM's port.

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.

Set up Cloudflare once

Publishing needs three things at Cloudflare:

  • A domain there (a zone) whose DNS records exe may manage.
  • A cloudflared tunnel, running on this machine or on another in your network. A remotely-managed tunnel (Zero Trust → Networks → Tunnels) works best: exe changes its rules through the API.
  • An API token. Create it at dash.cloudflare.com → API Tokens → Create Token → Custom token, with two permissions: Zone → DNS → Edit and Account → Cloudflare Tunnel → Edit. Add Zone → Analytics → Read for the Analytics app.

Then choose Special → Cloudflare Setup Wizard…, or Set Up… on the Cloudflare tab of Windows → Configuration, and give them to exe:

  1. Paste the token. Validate & Continue checks each answer against Cloudflare before the next step opens.
  2. Pick the account, the zone and the tunnel from the lists the wizard offers, or enter an ID by hand.
  3. Type the base domain. Apps publish as <name>.<this domain>: use the zone's own name (example.com) or a namespace under it, such as apps.example.com.
  4. On the last step, click Save Configuration. The settings apply at once, with no restart.

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.
  • A locally managed tunnel, one configured by a file on the cloudflared host, takes no rules from exe, and the wizard says so. Add one catch-all rule by hand that sends your names to exe's proxy, or recreate the tunnel in the Zero Trust dashboard. exe still manages the DNS records and the routes.

Publish a VM port

  1. Start your server in the VM, listening on 0.0.0.0. One that listens on 127.0.0.1 only cannot be published.
  2. Open the VM's window and click its Expose tab. Right-clicking the VM in the Virtual Machines window and choosing Expose Port… lands there too.
  3. Type the VM port and, if you like, a Subdomain. Left blank, the subdomain is the VM's name.
  4. Click Expose.

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.

See what is published, and unpublish

  • A VM's Services tab shows each published port of that VM with its address as a link, and an Unexpose button. Click it twice: it asks Sure? first.
  • Windows → My Apps shows every published name as an icon, whatever it leads to. Double-click one to open the site; right-click it for Open, Copy URL and Unpublish….
  • 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.

Publish from a terminal

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

What an agent can call

An agent that drives exe over HTTP has the whole of publishing:

ToCall
See what listens in a VMGET /v1/vms/{name}/ports
Publish a VM portPOST /v1/vms/{name}/expose with port and, optionally, subdomain
Publish a redirectPOST /v1/routes/redirect with host and target
Publish a service that is not in a VMPOST /v1/routes with host and backend
List the routesGET /v1/routes
Unpublish a nameDELETE /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.

Redirect a hostname

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
  • The name is your configured domain, one of its full subdomain names, or a name in another zone your Cloudflare token can edit, such as a second domain on the same account. Its DNS record goes in that zone; the tunnel and the proxy are the same.
  • The target is an HTTP(S) origin on another hostname, with no path, query, fragment or credentials.
  • exe creates the same DNS record and tunnel rule, then answers with 308 Permanent Redirect, preserving each request's path and query string: 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.

Publish a service that is not in a VM

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
  • The backend is an HTTP(S) origin with no path. Every request's own path and query reach it, with the hostname in Host, so one backend can serve several names.
  • The name follows the redirect's rule: in your domain, or in another zone your token can edit.
  • The same DNS record, tunnel rule and proxy route are created, and exe unexpose <host> removes them.

Publish a directory of files

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.
  • Downloads can resume: byte ranges are honoured. A disk image or archive (.dmg, .img, .toast, .iso, .sit, .zip, .bin, .hqx) is sent as a download rather than shown.
  • A folder that holds a file named .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.
  • The API: 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.

Publish the exe homepage

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.

Point a name at IPFS

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>.
  • Only a name exe has published can have one, and the path is /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.

Check the tunnel

  • The Cloudflare module in the Control Strip, bottom-left, shows tunnel health at a glance. The desktop describes its lamp and its menu.
  • Special → Cloudflare Status… shows the last check of the token, the DNS access and the tunnel, then the tunnel's replicas and connections, the live traffic of this machine's connector and the saved settings. Re-check Now checks again; Start Over… opens the wizard with empty fields.

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.

Local services for the desk

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.
  • A name is lowercase letters, digits and hyphens; its address is an HTTP(S) origin with no path.
  • The API token is checked here and never forwarded, whether it came in a header or as ?token=; nor are the desk's cookies. The service's cookies and CORS headers stay behind.
  • A change to services through PUT /v1/config takes effect at once.
Contents