Getting Started
A public address for something that is only listening on localhost.
The problem is the one Shiny.Net.HttpServer solves for an embedded server,
and the pieces are the same pieces: a pluggable ITunnelProvider, the Shiny relay at both ends, SSH
remote forwarding, zero-account quick tunnels, and Azure Relay. What changes is who runs them — here
it is the Aspire AppHost, on behalf of a resource in the app model.
| GitHub | |
| Downloads |
Packages
Section titled “Packages”All of these install into the AppHost project. Nothing is installed into the projects being published — a tunnelled service is not modified and never learns it is reachable from anywhere else.
Quick Start
Section titled “Quick Start”-
Install the package into your AppHost project
-
Publish a resource
var builder = DistributedApplication.CreateBuilder(args);var api = builder.AddProject<Projects.Api>("api");// A public HTTPS address. No account, nothing installed.api.WithQuickTunnel();builder.Build().Run(); -
Read the address off the tunnel resource in the dashboard, or hand it to the service that needs it
var tunnel = builder.AddQuickTunnel("public");builder.AddProject<Projects.Api>("api").WithTunnel(tunnel).WithTunnelUrl("PUBLIC_URL", tunnel);
The core idea
Section titled “The core idea”A tunnel is a resource whose public URL is its connection string. That single decision is what makes the rest work without introducing new concepts:
WithReference(tunnel)injects it asConnectionStrings__<name>WithTunnelUrl("PUBLIC_URL", tunnel)names the variable yourself- both wait for the tunnel to actually open before the referencing resource starts
A tunnel opens when its target’s endpoints are allocated, which happens before the target process is launched. A service may therefore reference its own tunnel — which is the usual case, since a webhook receiver has to hand out its own address, and an OAuth client has to register its own redirect URI.
Which provider
Section titled “Which provider”| Method | Package | Address | Needs |
|---|---|---|---|
WithQuickTunnel() |
….Ssh |
Assigned, changes on reconnect | Nothing at all |
WithSshTunnel(host, …) |
….Ssh |
Yours | An SSH server you can log into |
WithShinyRelayTunnel(relay) |
core | Yours | The relay, hosted here or on a VPS |
WithAzureRelayTunnel(cs) |
….AzureRelay |
Stable, yours | An Azure Relay namespace |
WithCloudflareTunnel() |
….Cloudflare |
Assigned, or your domain | Docker; an account for named tunnels |
WithNgrokTunnel(token) |
….Ngrok |
Assigned, or a reserved domain | Docker; an ngrok account |
The first four run inside the AppHost as managed code — no daemon, no binary to install, nothing to log into, and identical on every developer’s machine and in CI. The last two run the vendor’s agent in a container.
Two spellings, always
Section titled “Two spellings, always”Every provider offers both. AddXTunnel(...) on the builder returns the tunnel, for when something
needs to reference it; WithXTunnel(...) on a target resource creates and attaches it in one line
and returns the target, for when nothing does.
// Attach in one line — the tunnel resource is named "{target}-tunnel"api.WithQuickTunnel();
// Keep a handle, because something else needs the URLvar tunnel = builder.AddQuickTunnel("public");api.WithTunnel(tunnel);worker.WithTunnelUrl("API_PUBLIC_URL", tunnel);Pass name: explicitly when one resource has more than one tunnel — both default to
{target}-tunnel, and Aspire rejects duplicate resource names.
Things worth knowing
Section titled “Things worth knowing”- The target must be a cleartext endpoint. TLS terminates at the public end of the tunnel, so
what arrives is plain HTTP. The
httpendpoint is chosen by default for exactly that reason; name another withendpointNameonly if it is also cleartext. - Tunnels are excluded from the manifest. They are a development-time affordance for reaching a machine that is not on the internet; a deployed app has a real address.
- An assigned address changes on reconnect. An environment variable handed to a running process
cannot be revised afterwards, so
WithTunnelUrlresolves once and keeps that value. The dashboard shows the current one. - Anything reachable through a tunnel is reachable by anyone who has the URL. These hostnames are unguessable, not private. Put authentication in front of anything that matters.
AI Coding Assistant
Section titled “AI Coding Assistant”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny-aspire@shinyStep 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny-aspire@shinyRequirements
Section titled “Requirements”- .NET 10
- .NET Aspire 13.1+
- Docker (for the Cloudflare and ngrok packages only)


