Give your service a stable subdomain, a real upstream URL, and put it in git. Survives every
asd down/asd up.
Level: 2 · Reading time: 10 min
python3 -m http.server 3000 is fine.Already know this rung? Skip to Level 3 — multiple services where one host runs several services from one asd net apply.
Level 1's asd expose 3000 was ephemeral — kill the process, lose the
URL. That's fine for sharing a screen for an hour. For anything you'll
come back to tomorrow, you want the description of the service to live
in version control, and the URL to come back automatically when you
start the workspace.
The shape: one asd.yaml at the project root + one
packages/<service>/net.manifest.yaml per service. asd net apply
reads both, registers the service, configures Caddy, brings the
tunnel up. The URL doesn't change between runs, so you can put it in
a README.
In an empty (or new) directory:
1. The project-level config:
# asd.yaml
version: 1
project:
name: hello-project
domain: localhost
network:
caddy:
enable: true
tls:
enabled: true
auto: true
tunnels:
mode: caddy
2. The service manifest:
# packages/hello/net.manifest.yaml
id: hello
name: Hello
seedDefaultRegistry: true
endpoint:
url: "http://127.0.0.1:3000"
caddy:
publishPreferred: true
hostRoute:
host: "hello-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}"
tunnel:
public: true
env:
HELLO_URL: "${{ macro.exposedOrigin() }}"
3. Start your upstream + apply:
python3 -m http.server 3000 & # the thing you want to expose
asd net apply # register + route + tunnel
You'll see something like:
ℹ️ Tunnel mode (from ASD_NET_APPLY_TUNNEL): automatic
net:asd → routes:1 services:1
✅ Apply complete: 1 seeded, 1 routes
$ asd urls
🔗 Active workspace URLs:
hello
caddy: https://hello.localhost
tunnel: https://hello-xyz1.eu2.tn.example.com
direct: http://127.0.0.1:3000
$ curl -s https://hello-xyz1.eu2.tn.example.com/ | head -1
<h1>Hello, world.</h1>
Check .env — asd net apply wrote HELLO_URL for you:
$ grep ^HELLO_URL= .env
HELLO_URL=https://hello-xyz1.eu2.tn.example.com
Now stop and restart:
$ asd down
$ asd up # or: asd net apply
$ asd urls
# same URL as before
The hostname survives because the manifest declared it, not because
the OS handed out a random port.
Three things changed from Level 1:
You named the service. id: hello plus hostRoute.host: hello-…
means the subdomain is a property of the manifest, not the
command line. Anyone who clones your repo and runs asd net apply
gets hello-<their-client-id>.<their-tunnel-host>.
asd net apply runs from declared state. It diffs what your
manifests say against what registry.json and Caddy currently
hold, then converges. Re-running it is a no-op. Editing the
manifest and re-running is the whole development loop.
env: writes back to .env. Any service that needs to know
the public URL (frontend pointing at backend, OAuth callback
registration, README badge generator) reads it from .env. No
"look up the URL and hardcode it" step.
The two ${{ macro.… }} calls did the work that would otherwise have
been hand-editing per environment:
${{ macro.tunnelClientId() }} — your tunnel-server client ID.${{ macro.tunnelEndpoint() }} — your tunnel server's FQDN. SwitchFull macro reference: reference/net-manifest § Template macros.
net.manifest.yaml schema → reference/net-manifest.