paste this link into your ai. it will know what to do.
Expose a local service to the public internet using ngrok. Starts a tunnel, optionally adds OAuth or WAF via Traffic Policy. Handles HTTP services and raw TCP (SSH, RDP, databases). Use when asked to expose, tunnel, share, or make a local service publicly accessible, or to reach a machine that has no public IP. Also covers sharing work-in-progress with a specific person and the dev-server quirks that break a shared preview.
Expose Localhost
Expose a local service to the public internet using ngrok. Optionally add OAuth, OWASP protection, or rate limiting via Traffic Policy.
Prerequisites
- ngrok CLI installed (
ngrokcommand available) - Auth token configured (
ngrok config add-authtoken <TOKEN>)
Workflow
Step 1: Pre-flight & Configuration
Silently verify ngrok is ready:
ngrok config checkIf auth token missing, tell user to run: ngrok config add-authtoken <TOKEN> (get token at https://dashboard.ngrok.com/get-started/your-authtoken)
Then ask all questions upfront before doing anything:
Before I expose your service, I need a few details:
1. **Port**: I see your app runs on port 3000. Is that correct?
2. **Domain**: Use your dev domain, or do you have a custom domain?
3. **Access control**: Open access, or require login (Google/GitHub/etc.)?
4. **Save config?**: One-time setup, or save for reuse?Do NOT mention cloud endpoints, reserved domains, or internal endpoints — those are advanced concepts the user shouldn't need to think about.
Detecting the port: Check package.json scripts for --port, .env for PORT=, docker-compose.yml for port mappings.
Is it even HTTP? If the user says SSH, RDP, a database, or names a port like 22, 3389, 5432, or 3306, this is a raw TCP service — skip to Non-HTTP Services (TCP). The domain and access-control questions above do not apply there.
Domains: Most ngrok accounts have a free static dev domain (e.g., something.ngrok-free.dev). Running ngrok http PORT uses it automatically. Users can also provide a custom domain configured in the ngrok dashboard. Some accounts (especially new ones) may not have a dev domain yet — if ngrok fails with ERR_NGROK_15013, tell the user: "You don't have a dev domain yet. Claim your free one at https://dashboard.ngrok.com/domains — then we can try again."
If user requests OAuth, also ask: "Should only specific people be able to access it? I can restrict by email address or email domain."
After gathering answers, confirm and get a Y/n before proceeding.
Step 2: Start the Tunnel
No security (simplest)
ngrok http {PORT} &
sleep 3
curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"[^"]*"' | head -1With a specific domain, add --url https://{DOMAIN}.
With security (Traffic Policy)
Create the traffic policy file first, then start ngrok with it.
OAuth-only (default when user requests auth):
on_http_request:
- actions:
- type: oauth
config:
provider: googleReplace google with the chosen provider (google, github, microsoft, gitlab, linkedin, twitch).
If the user requests OAuth, default to OAuth-only. Do NOT add OWASP or rate limiting unless explicitly asked — OAuth already blocks unauthenticated access.
OAuth with email restriction — use a separate rule with a CEL expression to deny non-matching emails. Do NOT add an allow field to the OAuth action.
Single email:
on_http_request:
- actions:
- type: oauth
config:
provider: google
- expressions:
- "actions.ngrok.oauth.identity.email != 'user@example.com'"
actions:
- type: deny
config:
status_code: 403Email domain:
on_http_request:
- actions:
- type: oauth
config:
provider: google
- expressions:
- "!actions.ngrok.oauth.identity.email.endsWith('@your-company.com')"
actions:
- type: deny
config:
status_code: 403Multiple emails — name the identity explicitly on the left of in:
- expressions:
- "!(actions.ngrok.oauth.identity.email in ['a@x.com', 'b@x.com'])"
actions:
- type: deny
config:
status_code: 403Shared secret (when the person won't log in with an IdP — an external client, a reviewer without a company account):
on_http_request:
- actions:
- type: basic-auth
config:
credentials:
- "reviewer:{PASSWORD}"Hand them the URL and the password over a channel you already trust. If they have a stable IP, restrict-ips is an alternative with nothing to share.
Open-access hardening (no auth, but wants protection):
on_http_request:
- actions:
- type: rate-limit
config:
name: default-rate-limit
algorithm: sliding_window
capacity: 200
rate: "60s"
bucket_key:
- conn.client_ip
- type: owasp-crs-request
config:
on_error: halt
on_http_response:
- actions:
- type: owasp-crs-response
config:
on_error: haltAfter writing the policy file, start ngrok:
ngrok http {PORT} --traffic-policy-file .ngrok/traffic-policy.yml &
sleep 3
curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"[^"]*"' | head -1Add --url https://{DOMAIN} if using a specific domain.
Sharing a dev server? Vite, Next, and webpack reject requests whose Host header isn't localhost, and their hot-reload websockets need the ngrok domain allowed. Set this up before handing over the URL — see references/TROUBLESHOOTING.md.
Step 3: Handle Errors
If a traffic policy action fails due to plan limitations:
- Tell the user which specific action requires an upgrade
- Offer to remove that action from the policy and retry
- Do NOT suggest switching to cloud endpoints as a workaround
Step 4: Persistent Configuration (If Requested)
Save these files to the project:
.ngrok/traffic-policy.yml— the policy (if security was configured).ngrok/expose.sh:
#!/bin/bash
set -e
echo "Your service will be at: https://{DOMAIN}"
ngrok http {PORT} --url https://{DOMAIN} --traffic-policy-file .ngrok/traffic-policy.ymlOmit --traffic-policy-file if no policy. Omit --url if no specific domain.
Optionally add to package.json:
{ "scripts": { "tunnel": "bash .ngrok/expose.sh" } }Teardown
pkill ngrokNon-HTTP Services (TCP)
For SSH, RDP, or a database, use ngrok tcp instead of ngrok http.
ngrok tcp {PORT} &
sleep 3
curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"[^"]*"' | head -1ngrok returns an address like tcp://1.tcp.ngrok.io:12345. The host and port are used separately by the client:
# local sshd on port 22, reached through the address above
ssh -p 12345 {USER}@1.tcp.ngrok.ioCommon ports: SSH 22, RDP 3389, PostgreSQL 5432, MySQL 3306.
The address changes on every restart. For one that persists, reserve it first. ngrok assigns the hostname and port — unlike domains, you cannot choose them:
ngrok api reserved-addrs create --description "SSH for {NAME}" --region us
ngrok tcp {PORT} --url tcp://{ADDR}Reserved TCP addresses need a paid plan and keep billing until deleted.
Access control is different here. OAuth and OWASP are HTTP-only and will not work. TCP policies run in the on_tcp_connect phase, where restrict-ips is the usual control:
on_tcp_connect:
- actions:
- type: restrict-ips
config:
enforce: true
allow:
- 203.0.113.0/24Attach it the same way: ngrok tcp {PORT} --traffic-policy-file .ngrok/traffic-policy.yml
Cloud Endpoints (Advanced)
Only use if the user explicitly needs a URL that persists after the agent stops (e.g., webhooks, long-lived integrations).
Requires the ngrok MCP server or an API key. If the user manages ngrok as code, add the endpoint there instead. If not, and the MCP server is connected, use it and do not ask for an API key: gw_reserve_domain and gw_create_cloud_endpoint replace steps 1 and 2, and delete_resource replaces step 4. Otherwise the user sets an API key with ngrok config add-api-key <KEY> (from https://dashboard.ngrok.com/api-keys). See ngrok-surfaces for the order to pick in.
- Reserve domain:
ngrok api reserved-domains create --domain "{DOMAIN}" - Create cloud endpoint with a traffic policy that includes
forward-internalas the last action:
ngrok api endpoints create --url "https://{DOMAIN}" --bindings public --traffic-policy "$(cat .ngrok/traffic-policy.yml)"The traffic policy must end with:
- type: forward-internal
config:
url: https://{NAME}.internal- Start the internal agent:
ngrok http {PORT} --url https://{NAME}.internal - Teardown:
ngrok api endpoints delete {ENDPOINT_ID}
Related Skills
This skill covers giving a service a public URL. Neighbouring jobs have their own skills — hand off rather than reimplementing them here:
- Auth, rate limits, IP rules, or a maintenance page in general —
secure-endpoint. (Sharing a preview with one named person is handled here, in Step 2.)
- It should receive webhooks —
receive-webhooksverifies provider signatures
at the edge and can keep the receiver off the public internet.
- It is an MCP server —
run-mcp-gatewayhandles per-provider credentials. - One endpoint per sandbox, tenant, or device, created by a controlplane —
provision-sandbox-access.
- Something is broken (
ERR_NGROK_*, a 400 through the URL but not on
localhost) — troubleshoot-ngrok.
- Setup or auth problems —
ngrok-setup.
For the underlying model — endpoint types, HTTP vs TCP, and the full Traffic Policy action set — see ngrok-engine. For applying config on a surface other than the CLI (SDKs, REST API, Terraform, Kubernetes), see ngrok-surfaces.
keep it where your ai can reach it.
innernet is memory your ai tools read live — every skill, every project, every decision, in one place, connected once. save this skill to yours, or publish one of your own as a link like this.