GITHUB ACTIONS nodetonet.com
Integration GitHub Actions CI/CD E2E Webhooks

Nodetonet in GitHub Actions — expose your test server for E2E and webhook tests

Run E2E and webhook tests against a real public URL inside GitHub Actions using Nodetonet HTTP tunnels — no static server, no paid plan, idle tunnels cost nothing.

N Nodetonet Team
May 15, 2026 9 min read

The most common reason to want a tunnel in CI is "my E2E test needs the world to reach my dev server." Cypress / Playwright run against localhost:3000 in the same job, but webhook providers (Stripe, GitHub, Shopify) need a real public URL, and so do third-party scanners like Lighthouse CI. A Nodetonet HTTP tunnel inside a workflow takes roughly three seconds to start and half a second to tear down — well under any test budget.

TL;DR: Two curl calls — one POST to open the tunnel, one DELETE to close it — give you a public HTTPS URL for the lifetime of a CI job, for a fraction of a cent per run. Idle tunnels cost nothing, and Nodetonet uses prepaid credit with no monthly subscription.

We don't yet ship an official nodetonet/cli-action; that is on the roadmap. For now, two curl calls give you 95% of what an action would do, and they are easier to debug. The REST API is the canonical interface and will keep working forever.

Why use a tunnel in CI at all?

Runners on GitHub-hosted machines sit behind ephemeral NAT — there is no way for an external service to reach your test server directly. A Nodetonet tunnel is a persistent reverse connection from our edge to your runner: once open, any request to https://<subdomain>.nodetonet.com is forwarded to your localhost. No port-forwarding, no firewall rules, no reserved IP needed.

Compare the main options available to CI teams today:

ApproachCost modelSetup effortWebhook-readyCustom domain
Nodetonet tunnelPrepaid credit, per secondTwo curl callsYesYes
ngrok free tierFree (limited sessions)One binary downloadYes (limited)Paid plan only
Cloudflare TunnelFree (persistent daemon)Daemon install + DNSYesYes (own domain)
Dedicated staging serverMonthly VPS cost, always onProvision + maintainYesYes
Public S3/CDN static deployStorage + transfer feesBuild pipeline changeNoYes

Nodetonet's edge advantage is that tunnels compose with mobile proxies and rotating proxies you may already run on the same panel — one prepaid balance, one dashboard, one API key. If your E2E suite also needs a clean mobile IP for outbound requests, you do not need a second vendor. See also Nodetonet vs ngrok and Nodetonet vs Cloudflare Tunnel for a detailed feature comparison.

The pattern in three steps

  1. At job startPOST /api/v1/proxies with the test server port. Capture the returned URL into $GITHUB_ENV.
  2. During the job — your test runner uses $TUNNEL_URL wherever it would have used http://localhost.
  3. At job endDELETE /api/v1/proxies/{id} in an if: always() step so cleanup runs even when tests fail.

Even if step 3 does not run (canceled job, runner died), Nodetonet auto-stops idle tunnels after 30 minutes of no traffic, and stopped tunnels cost nothing. The blast radius of a leaked tunnel is "a dangling URL until you notice it." Learn more about monitoring tunnel health and audit logs for CI usage.

A working workflow file

Drop this in .github/workflows/e2e.yml. It assumes your test server boots in the background, listens on :3000, and your tests respect TARGET_URL:

name: E2E with Nodetonet tunnel
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - name: Install deps
        run: npm ci

      - name: Boot test server (background)
        run: npm run start:test &
        env:
          PORT: 3000

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Open Nodetonet tunnel
        env:
          NTN_TOKEN: ${{ secrets.NTN_TOKEN }}
        run: |
          SUB="ci-${{ github.run_id }}"
          RESP=$(curl -fsS -X POST https://nodetonet.com/api/v1/proxies             -H "Authorization: Bearer $NTN_TOKEN"             -H "Content-Type: application/json"             -d "{\"subdomain\":\"$SUB\",\"target_host\":\"127.0.0.1\",\"target_port\":3000}")
          echo "TUNNEL_ID=$(echo $RESP | jq -r .id)"  >> $GITHUB_ENV
          echo "TUNNEL_URL=$(echo $RESP | jq -r .url)" >> $GITHUB_ENV

      - name: Run Playwright against public URL
        env:
          TARGET_URL: ${{ env.TUNNEL_URL }}
        run: npx playwright test

      - name: Close Nodetonet tunnel
        if: always()
        env:
          NTN_TOKEN: ${{ secrets.NTN_TOKEN }}
        run: |
          curl -fsS -X DELETE             -H "Authorization: Bearer $NTN_TOKEN"             https://nodetonet.com/api/v1/proxies/${{ env.TUNNEL_ID }} || true

The jq binary is pre-installed on all ubuntu-latest runners. If you are on a self-hosted runner that lacks it, install with sudo apt-get install -y jq before the tunnel step.

Webhook tests — the killer use case

If you are testing a Stripe, GitHub, or Twilio webhook integration, the provider must reach your server. localhost obviously will not do, and local-runner tricks like act do not help because the third-party service lives in its own data center.

The flow is identical to E2E — open a tunnel, register the webhook endpoint as $TUNNEL_URL/webhooks/stripe, run the test that triggers the event, assert the outcome. The only twist is wanting a predictable subdomain per branch so cleanup is easy if a run dies mid-way. Use github.head_ref for PR jobs:

      - name: Open per-branch tunnel
        env:
          NTN_TOKEN: ${{ secrets.NTN_TOKEN }}
        run: |
          SUB="webhook-$(echo ${{ github.head_ref || github.ref_name }}                        | tr '/' '-' | tr -cd 'a-z0-9-' | cut -c1-40)"
          # ... same POST as above

For a deeper look at the lifecycle of Nodetonet tunnels — including how TLS certificates are issued automatically and how custom domains work — read those companion posts.

Storing the API token securely

Two things matter for CI token hygiene:

If you use deployment environments (staging, prod), use a separate token per environment so audit logs show exactly which CI run opened which tunnel.

Parallel jobs and matrix builds

When you run a matrix build, each parallel job needs its own tunnel. The subdomain approach already handles this: if you include the matrix variable in the subdomain (e.g., "ci-${{ github.run_id }}-${{ matrix.node }}"), each shard gets a unique public URL and there is no collision. The same DELETE in if: always() closes only that shard's tunnel.

For large fleet builds where you want per-client controls — quotas, thread limits, IP allow-lists — see the per-client auth and thread limits posts, which describe the same model for HTTP tunnels.

Cost — what does this actually cost?

Nodetonet uses prepaid credit, billed per second of active tunnel time. A tunnel that lives for a 10-minute CI job costs a small fraction of a cent per run. A repo running E2E on every PR push (say 150 CI runs per month) typically spends well under a dollar per month on tunnels. Even a hyperactive monorepo with thousands of runs per month stays cheap. Idle tunnels — those with no active traffic — cost nothing at all. You only pay while the tunnel is open and serving requests.

For an honest comparison of costs across providers, check our Nodetonet vs ngrok and Nodetonet vs Cloudflare Tunnel pages.

Coming: an official GitHub Action

The pattern above is reliable but verbose. An official nodetonet/cli-action@v1 is on the roadmap and will collapse the four steps (POST, capture env, run tests, DELETE) into:

      - uses: nodetonet/cli-action@v1
        with:
          token: ${{ secrets.NTN_TOKEN }}
          port: 3000
        # exports TUNNEL_URL automatically, auto-cleans on job end

      - run: npx playwright test
        env:
          TARGET_URL: ${{ env.TUNNEL_URL }}

The curl pattern above will keep working forever — it is the canonical REST interface — but the action will be the one-liner you reach for first. Contact support or join the Discord to get notified when it ships.

Get started

Ready? Create a free account, generate an API token, paste the workflow above, and push a PR. Your tunnel opens in seconds. Explore the HTTP tunnels feature page for the full tunnel reference, or browse the proxy checker to verify your connection from any endpoint.

Frequently asked questions

What happens if a CI job dies and the cleanup step never runs?
Nodetonet auto-stops tunnels after 30 minutes of no traffic, and stopped tunnels are free. The orphan tunnel stays in your panel until you delete it manually, but it will not bill you. For an extra safety net, use a dedicated scoped token per CI environment so you can see and bulk-delete all tunnels opened by that environment.
Can I use a custom domain for CI tunnels?
Yes. Configure a custom subdomain via the panel — see the custom domain guide — and reuse it across runs with a stable subdomain name. Most teams use the auto-generated {name}.nodetonet.com for ephemeral CI tunnels, reserving custom domains for staging and production.
How do I keep the public URL stable across CI runs?
Reserve a subdomain in the panel and reuse it in your workflow by passing the same "subdomain" value in the POST body. The API returns 200 if the subdomain already exists, or 201 if it is new — either way you get the same URL back. This is useful when an external webhook provider needs a stable destination it cannot reconfigure between runs.
Can I run Cypress and Playwright tests with a Nodetonet tunnel?
Yes. Both frameworks accept a baseURL or TARGET_URL environment variable, so you just set that to the tunnel URL and your tests run identically against the public endpoint. Playwright's playwright.config.ts supports baseURL: process.env.TARGET_URL || "http://localhost:3000", making local and CI runs seamless.
Does Nodetonet support HTTPS out of the box for CI tunnels?
Yes. Every tunnel URL Nodetonet issues is HTTPS by default, with a valid TLS certificate issued automatically — no extra configuration required. See the Let's Encrypt auto-renewal post for the certificate lifecycle details.
How is Nodetonet different from ngrok for CI?
Both give you a public URL for localhost. Nodetonet charges per second of active tunnel time with no session limits or connection caps on paid plans, and it shares a billing account and dashboard with any mobile proxies or rotating proxies you run. See the full Nodetonet vs ngrok comparison for a side-by-side breakdown.
Can I use Nodetonet tunnels with self-hosted GitHub Actions runners?
Yes. The tunnel is opened by an outbound HTTPS request from your runner to the Nodetonet API, so it works on any machine with internet access — GitHub-hosted, self-hosted, or even a local workstation. The only dependency is curl and jq, both standard on Linux runners.

Add Nodetonet to your CI in five minutes

Paste the workflow snippet above, set NTN_TOKEN as a secret, push a PR. Your tunnel opens before your tests do.