Explita InfraExplita Infra
Tunnel

Self-Hosting Guide

Step-by-step instructions to self-host and deploy your own Explita Tunnel edge gateway

Self-Hosting Explita Tunnel

Because Explita Tunnel is fully open source, you can host your own dedicated edge gateway cluster on any Linux VPS, Docker server, or Kubernetes environment.

Self-hosting gives your team:

  • Complete Data Sovereignty: All traffic passes exclusively through your own infrastructure.
  • Custom Branded Domains: Use *.tunnel.yourcompany.com instead of third-party domains.
  • No Shared Resource Contention: Full control over network throughput, concurrency limits, and rate limits.
  • Internal Network Bridging: Securely bridge local developer environments directly into private VPCs.

📋 Prerequisites

Before setting up your gateway server, ensure you have:

  1. A Linux server (Ubuntu 22.04 LTS or newer recommended) with a public static IP address.
  2. A registered domain name (e.g. yourdomain.com).
  3. DNS access to configure a wildcard record:
    • A record: tunnel.yourdomain.com -> <Server-Public-IP>
    • A or CNAME record: *.tunnel.yourdomain.com -> <Server-Public-IP>
  4. Docker & Docker Compose or Node.js v18+ with pnpm.

⚙️ Environment Variables

The tunnel gateway server reads configuration from environment variables or a root .env file:

VariableTypeDefaultDescription
HTTP_PORTnumber39755Internal TCP port for the Fastify server
TUNNEL_DOMAINstringlocalhost:39755The canonical base hostname (e.g. tunnel.yourdomain.com)
AUTH_TOKENSstringNoneComma-separated list of pre-shared tokens to secure the gateway with zero database dependencies
RESERVED_SUBDOMAINSstringNoneComma-separated list of additional subdomains to disallow
DOCS_URLstringNoneURL to redirect apex domain visitors (e.g. https://docs.yourcompany.com)
LOG_INFObooleanfalseSet to "true" to enable verbose connection logs in stdout
DATABASE_URLstringNoneOptional PostgreSQL connection string (when using @explita/tunnel-server-plugin)
INTERNAL_SECRETstringNoneSecret token required to query telemetry at GET /_api/stats (when plugin is enabled)

🐳 Docker Deployment

The fastest way to deploy the tunnel server is using Docker Compose.

1. Clone & Build the Repository

First, clone the repository to your server and build the application:

git clone https://github.com/explita/tunnel-server.git /opt/explita/tunnel
cd /opt/explita/tunnel

# Install dependencies and build TypeScript
pnpm install
pnpm build

2. Create docker-compose.yml

In the root of the cloned repository (/opt/explita/tunnel), create docker-compose.yml:

services:
  tunnel-gateway:
    image: node:20-alpine
    container_name: explita-tunnel-gateway
    restart: unless-stopped
    working_dir: /app
    ports:
      - "127.0.0.1:39755:39755"
    environment:
      - NODE_ENV=production
      - HTTP_PORT=39755
      - TUNNEL_DOMAIN=tunnel.yourdomain.com
      - LOG_INFO=true
    volumes:
      - .:/app
    command: ["node", "dist/server.js"]

[!IMPORTANT] The docker-compose.yml file must be located in the same directory where you cloned the repository (/opt/explita/tunnel). This ensures volumes: - .:/app properly mounts the compiled project files, dist/, and dependencies into the container.

3. Start the Gateway Container

docker compose up -d

[!NOTE] We bind the gateway port to 127.0.0.1:39755 so that public ingress traffic is strictly routed through an edge reverse proxy (such as Caddy or Nginx) that handles TLS termination and wildcard certificates.


🔒 Edge Ingress with Caddy & Wildcard TLS

Explita Tunnel requires wildcard HTTPS (*.tunnel.yourdomain.com) so any dynamic subdomain requested by developers receives an automatic valid TLS certificate.

Caddy is the ideal edge proxy because it automates Let's Encrypt wildcard certificates via DNS-01 challenges (e.g. Cloudflare DNS plugin) without manual cron renew scripts.

Sample Caddyfile Configuration

Below is a production Caddyfile configuration:

# Replace with your Cloudflare API token or your DNS provider credentials
*.tunnel.yourdomain.com, tunnel.yourdomain.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }

    # Proxy all HTTP, SSE, and WebSocket traffic to the local tunnel gateway
    reverse_proxy 127.0.0.1:39755 {
        # Preserve original client IP and host details
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Enable immediate streaming for Server-Sent Events (SSE)
        flush_interval -1
    }
}

[!IMPORTANT] Because WebSocket connections upgrade via HTTP 101 Switching Protocols, Caddy automatically handles full-duplex WebSocket proxying without additional directive blocks.


💻 Manual Node.js / Linux Service Deployment

If you prefer deploying directly via systemd and Node.js without containers:

1. Clone & Build the Gateway

git clone https://github.com/explita/tunnel-server.git /opt/explita/tunnel
cd /opt/explita/tunnel

# Install dependencies and build TypeScript
pnpm install
pnpm build

2. Configure Environment

Create /opt/explita/tunnel/.env:

NODE_ENV=production
HTTP_PORT=39755
TUNNEL_DOMAIN=tunnel.yourdomain.com
LOG_INFO=true

3. Create a Systemd Service

Create /etc/systemd/system/explita-tunnel.service:

[Unit]
Description=Explita Tunnel Gateway Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/explita/tunnel
ExecStart=/usr/bin/node /opt/explita/tunnel/dist/server.js
Restart=always
RestartSec=5
EnvironmentFile=/opt/explita/tunnel/.env
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now explita-tunnel
sudo systemctl status explita-tunnel

🩺 Verifying Health & Ingress

The gateway exposes a dedicated JSON health endpoint at /_health:

curl -i https://tunnel.yourdomain.com/_health

Expected response:

{
  "status": "healthy",
  "version": "1.0.0",
  "activeTunnels": 0,
  "uptime": 1420.5,
  "memory": {
    "rss": 42100000,
    "heapUsed": 21500000
  }
}

Apex domain requests (https://tunnel.yourdomain.com/) return a 200 OK service descriptor or automatically 302 redirect to your DOCS_URL if configured.


🔒 Securing Your Gateway

By default, the gateway operates in open mode (anyone who can reach your gateway address can open a tunnel). You can secure your gateway using either lightweight pre-shared tokens or the full database plugin:

Option A: Pre-Shared Tokens (Zero Database Required)

If you are running the gateway for personal use, a small team, or CI/CD pipelines and do not want to run a database, define one or more pre-shared static tokens in your gateway .env or Docker environment:

# Comma-separated list of valid access tokens
AUTH_TOKENS="sec_token_alice,sec_token_bob,sec_token_ci"

When AUTH_TOKENS is defined:

  • The gateway automatically locks down and enforces authentication on all connections.
  • Any connection without a matching token is rejected immediately with WebSocket code 1008 ("Invalid token").
  • Developers simply connect using -t, --token:
    eta 3000 -s my-app -t sec_token_alice
    Or save it permanently on their machine:
    eta config set token sec_token_alice

Option B: Multi-User Database Authentication

If you need passwordless 6-digit email OTP login, automated machine token issuance, and persistent developer accounts in PostgreSQL, attach the official plugin:

👉 See the Auth & Telemetry Plugin Guide for step-by-step setup using tunnel.config.js.


🔌 Connecting Clients to Your Self-Hosted Gateway

Once your gateway is operational, developers can connect their local CLI agents to your self-hosted infrastructure.

To configure your self-hosted gateway as the permanent default on a developer machine, set it globally in ~/.explita/config.json:

# Set your custom gateway globally using the eta binary
eta config set server wss://tunnel.yourdomain.com/_connect

# Or using the unified explita binary
explita config set tunnel.server wss://tunnel.yourdomain.com/_connect

Verify your active global configuration:

eta config list

2. One-Off Flag Connection

Alternatively, point a single session to your custom gateway using the -S, --server flag:

# Connect local port 3000 to your self-hosted gateway
eta 3000 -s my-app -S wss://tunnel.yourdomain.com/_connect

3. Project-Level Configuration (explita.json)

To lock in port and subdomain defaults for a shared repository or team project, generate an explita.json:

{
  "$schema": "https://infra.explita.ng/config/schema.json",
  "tunnel": {
    "port": 3000,
    "subdomain": "dev-team"
  }
}

Now developers on your team simply run eta with zero flags.

On this page