/community/expose localhost
innernet
docsskillscommunitycompanydeveloperssecurityprivacytermscontact

a skill by ngrok, brought here by kt

expose localhost

paste this link into your ai. it will know what to do.

/skills/ngrok-expose-localhost

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.

kindskill
version2.1
last tended2026-11-06
written byngrok
sourcegithub.com/ngrok/skills
kept currentwith its source · last read 2026-11-13
brought here bykt
reuseMIT
yours?it stays under your name. to change it or take it down, write to connect@innernet.live
reading6 min

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 (ngrok command available)
  • Auth token configured (ngrok config add-authtoken <TOKEN>)

Workflow

Step 1: Pre-flight & Configuration

Silently verify ngrok is ready:

ngrok config check

If 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 -1

With 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: google

Replace 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: 403

Email 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: 403

Multiple 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: 403

Shared 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: halt

After 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 -1

Add --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:

  1. 1Tell the user which specific action requires an upgrade
  2. 2Offer to remove that action from the policy and retry
  3. 3Do 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.yml

Omit --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 ngrok

Non-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 -1

ngrok 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.io

Common 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/24

Attach 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.

  1. 1Reserve domain: ngrok api reserved-domains create --domain "{DOMAIN}"
  2. 2Create cloud endpoint with a traffic policy that includes forward-internal as 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
  1. 1Start the internal agent: ngrok http {PORT} --url https://{NAME}.internal
  2. 2Teardown: 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-webhooks verifies provider signatures

at the edge and can keep the receiver off the public internet.

  • It is an MCP server — run-mcp-gateway handles 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.

save it to your innernetsee every skill