@gagandeep023/expose-tunnel
v0.5.3
Published
Self-hosted tunnel to expose local servers to the internet. An ngrok/localtunnel alternative you run on your own infrastructure.
Maintainers
Readme
@gagandeep023/expose-tunnel
A self-hosted tunnel to expose local servers to the internet. An alternative to ngrok and localtunnel that runs on your own infrastructure.
How It Works
Browser
| HTTPS myapp.tunnel.yourdomain.com
v
Relay server on your VPS
| WebSocket, opened OUTBOUND by your machine,
| so no inbound port or firewall rule is needed
v
expose-tunnel client on your machine
| HTTP
v
localhost:3000- The client connects to your relay server via WebSocket
- The relay server assigns a public subdomain (e.g.,
abc123.tunnel.yourdomain.com) - When external traffic hits that subdomain, the relay server pipes it through the WebSocket to your machine
- Your client forwards the request to
localhost:<port>and sends the response back
WebSocket Support
Since v0.5.0, WebSocket upgrades pass through end to end, so wss://<subdomain>.tunnel.yourdomain.com
works alongside plain HTTP. Socket.IO and raw ws clients connect over the websocket transport instead
of falling back to HTTP polling.
Each external WebSocket connection is multiplexed over the same control connection with its own connection id. Text and binary frames both round-trip, and closes propagate in both directions. When the tunnel client disconnects, every bridged socket is closed; clients that auto-reconnect (like Socket.IO) re-handshake on their own.
Requires both the relay server and the client to run v0.5.0 or later. Your relay's reverse proxy must
forward upgrades (proxy_http_version 1.1, Upgrade/Connection headers) and use a long
proxy_read_timeout so idle sockets are not cut. See SELF-HOSTING-GUIDE.md.
Raw TCP/TLS tunneling is still out of scope; this covers WebSockets carried over HTTP.
Installation
npm install @gagandeep023/expose-tunnelOr run directly with npx (no install needed):
npx @gagandeep023/expose-tunnel \
--port 3000 \
--server wss://tunnel.yourdomain.com \
--api-key sk_your_keyQuick Start
1. Set environment variables (recommended)
export EXPOSE_TUNNEL_SERVER=wss://tunnel.yourdomain.com
export EXPOSE_TUNNEL_API_KEY=sk_your_key_here2. Expose a local port
npx @gagandeep023/expose-tunnel --port 3000Output:
[expose-tunnel] Tunnel established!
[expose-tunnel] Public URL: https://abc123.tunnel.yourdomain.com
[expose-tunnel] Forwarding to: http://localhost:3000
[expose-tunnel] Press Ctrl+C to close the tunnel.CLI Usage Examples
Basic: expose a port (env vars set)
# Requires EXPOSE_TUNNEL_SERVER and EXPOSE_TUNNEL_API_KEY env vars
npx @gagandeep023/expose-tunnel --port 3000With a custom subdomain
npx @gagandeep023/expose-tunnel --port 3000 --subdomain myapp
# -> https://myapp.tunnel.yourdomain.comWithout a subdomain (random assigned)
npx @gagandeep023/expose-tunnel --port 3000
# -> https://a1b2c3d4.tunnel.yourdomain.comWith --server flag (no env var needed)
npx @gagandeep023/expose-tunnel \
--port 3000 \
--server wss://tunnel.yourdomain.comWith --server and --subdomain
npx @gagandeep023/expose-tunnel \
--port 3000 \
--server wss://tunnel.yourdomain.com \
--subdomain myappWith --api-key flag (no env var needed)
npx @gagandeep023/expose-tunnel \
--port 3000 \
--server wss://tunnel.yourdomain.com \
--api-key sk_your_keyAll flags, no env vars
npx @gagandeep023/expose-tunnel \
--port 3000 \
--server wss://tunnel.yourdomain.com \
--subdomain myapp \
--api-key sk_your_key_hereCustom local host
# Forward to a different local hostname (default: localhost)
npx @gagandeep023/expose-tunnel --port 3000 --local-host 0.0.0.0Expose different ports
# Expose a React dev server
npx @gagandeep023/expose-tunnel --port 5173 --subdomain react-app
# Expose an Express backend
npx @gagandeep023/expose-tunnel --port 3001 --subdomain api
# Expose a database admin panel
npx @gagandeep023/expose-tunnel --port 8080 --subdomain adminCLI Reference
Usage: expose-tunnel [options]
Expose local servers to the internet via your own relay server
Options:
-V, --version output version number
-p, --port <number> Local port to expose (required)
-s, --subdomain <name> Request a specific subdomain
--server <url> Relay server WebSocket URL
(or EXPOSE_TUNNEL_SERVER)
--api-key <key> API key (or EXPOSE_TUNNEL_API_KEY)
--local-host <host> Local host to proxy to (default: localhost)
-h, --help display help for commandProgrammatic API
Basic usage
import { exposeTunnel } from '@gagandeep023/expose-tunnel';
const tunnel = await exposeTunnel({
port: 3000,
server: 'wss://tunnel.yourdomain.com',
apiKey: 'sk_your_key_here',
});
console.log(`Public URL: ${tunnel.url}`);
// Close when done
await tunnel.close();With subdomain
const tunnel = await exposeTunnel({
port: 3000,
server: 'wss://tunnel.yourdomain.com',
apiKey: 'sk_your_key_here',
subdomain: 'myapp',
});
console.log(tunnel.url);
// -> https://myapp.tunnel.yourdomain.comWithout subdomain (random)
const tunnel = await exposeTunnel({
port: 3000,
server: 'wss://tunnel.yourdomain.com',
apiKey: 'sk_your_key_here',
});
console.log(tunnel.url);
// -> https://a1b2c3d4.tunnel.yourdomain.comUsing env vars (no server/apiKey in code)
// Set EXPOSE_TUNNEL_SERVER and EXPOSE_TUNNEL_API_KEY env vars first
const tunnel = await exposeTunnel({ port: 3000 });Event listeners
const tunnel = await exposeTunnel({
port: 3000,
server: 'wss://tunnel.yourdomain.com',
apiKey: 'sk_your_key_here',
});
// Log incoming requests
tunnel.on('request', (method, path, status) => {
console.log(`${method} ${path} -> ${status}`);
});
// Handle errors
tunnel.on('error', (err) => {
console.error(err);
});
// Handle tunnel close
tunnel.on('close', () => {
console.log('Tunnel closed');
});
await tunnel.close();TunnelClient class (advanced)
import { TunnelClient } from '@gagandeep023/expose-tunnel';
const client = new TunnelClient({
port: 3000,
server: 'wss://tunnel.yourdomain.com',
apiKey: 'sk_your_key_here',
subdomain: 'myapp',
});
const instance = await client.connect();
console.log(instance.url);
// ... use the tunnel
await client.close();API Reference
exposeTunnel(options)
Creates a tunnel and returns a TunnelInstance.
Parameters:
| Parameter | Type | Required | Default | Description |
|-------------|----------|----------|-----------------------------------|--------------------------------------|
| port | number | Yes | | Local port to expose |
| server | string | Yes* | EXPOSE_TUNNEL_SERVER env var | Relay server WebSocket URL |
| apiKey | string | Yes* | EXPOSE_TUNNEL_API_KEY env var | Authentication key |
| subdomain | string | No | Random 8-char | Requested subdomain |
| localHost | string | No | localhost | Local hostname to proxy requests to |
*Can be provided via env var instead.
Returns: Promise<TunnelInstance>
TunnelInstance
| Property/Method | Type | Description |
|-----------------|-------------------------|--------------------------------------------------|
| url | string | Public HTTPS URL of the tunnel |
| subdomain | string | Assigned subdomain |
| close() | () => Promise<void> | Closes the tunnel connection |
| on(event, fn) | EventEmitter | Listen for request, error, or close events |
Environment Variables
Client
| Variable | Description |
|--------------------------|--------------------------------------------------|
| EXPOSE_TUNNEL_SERVER | Relay server WebSocket URL (e.g., wss://tunnel.yourdomain.com) |
| EXPOSE_TUNNEL_API_KEY | API key for authenticating with the relay server |
Relay Server
| Variable | Description | Default |
|-------------------|----------------------------------------------------------|----------------------------|
| RELAY_PORT | Port the relay server listens on | 4040 |
| API_KEYS | Comma-separated list of valid API keys | (required) |
| TUNNEL_DOMAIN | Base domain for tunnel subdomains | tunnel.gagandeep023.com |
| MAX_TUNNELS | Maximum number of concurrent tunnel connections allowed | 10 |
Self-Hosting the Relay Server
The package includes a relay server you can deploy on your own infrastructure. See SELF-HOSTING-GUIDE.md for the full step-by-step deployment guide.
Quick overview
- Deploy on any VPS (EC2, DigitalOcean, etc.)
- Set up wildcard DNS (
*.tunnel.yourdomain.com-> your server IP) - Configure Nginx with wildcard SSL
- Start the relay server with PM2
Requirements
- Node.js 18+
- A domain with wildcard DNS (e.g.,
*.tunnel.yourdomain.com) - Nginx with wildcard SSL certificate
- PM2 (recommended) for process management
Quick Setup
mkdir expose-tunnel-server && cd expose-tunnel-server
npm init -y
npm install @gagandeep023/expose-tunnelCreate .env:
RELAY_PORT=4040
API_KEYS=sk_your_generated_key
TUNNEL_DOMAIN=tunnel.yourdomain.com
MAX_TUNNELS=10Generate an API key:
node -e "console.log('sk_' +
require('crypto').randomBytes(24).toString('hex'))"Create start.js:
const fs = require('fs');
const path = require('path');
const envFile = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
envFile.split('\n').forEach(line => {
const [key, ...val] = line.split('=');
if (key && val.length) process.env[key.trim()] = val.join('=').trim();
});
require('@gagandeep023/expose-tunnel/dist/server/index.js');Start it:
pm2 start start.js --name expose-tunnel-relayHealth Check
curl https://tunnel.yourdomain.com/health
# { "status": "ok", "tunnels": 0, "maxTunnels": 10 }TypeScript
Full TypeScript support with exported types:
import type {
TunnelOptions,
TunnelInstance,
TunnelRequest,
TunnelResponse,
} from '@gagandeep023/expose-tunnel';Requests and feedback
Ideas and questions go to Discussions, bugs to Issues.
License
MIT
