ngrok alternative - easy self host tunnel to expose local website
  • TypeScript 90.1%
  • Shell 9.3%
  • Dockerfile 0.6%
Find a file
2026-06-19 20:59:05 +04:00
.github refactor: smaller image 2026-06-19 20:58:44 +04:00
art docs: add README artwork 2026-04-20 15:29:21 +04:00
src refactor: smaller image 2026-06-19 20:58:44 +04:00
.bun-version chore: pin bun version to 1.3.13 2026-04-21 21:17:33 +04:00
.dockerignore feat: initial commit 2026-04-20 00:47:07 +04:00
.gitignore feat: initial commit 2026-04-20 00:47:07 +04:00
bun.lock fix(deps): update dependency @clack/prompts to v1.3.0 (#3) 2026-04-30 01:07:11 +04:00
Dockerfile refactor: smaller image 2026-06-19 20:58:44 +04:00
index.ts feat: add better regex + uninstall + scriptable version 2026-04-30 00:19:56 +04:00
install.sh fix: prevent redownload if version is same 2026-04-30 00:41:28 +04:00
LICENSE docs: add MIT license 2026-04-20 15:27:59 +04:00
package.json chore: bump release: 1.5.1 2026-06-19 20:59:05 +04:00
README.md docs: improve documentation 2026-04-30 01:03:42 +04:00
renovate.json chore: add renovate.json (#1) 2026-04-29 23:42:11 +04:00
Taskfile.yml refactor: smaller image 2026-06-19 20:58:44 +04:00
tsconfig.json feat: initial commit 2026-04-20 00:47:07 +04:00

w

Image

A self-hosted alternative to ngrok to tunnel local HTTP to the web.

Comparison

Feature w-share ngrok Cloudflare Tunnel localhost.run serveo.net
Self-hosted Yes No No No No
No account required Yes No No Yes Yes
Own domain control Yes ⚠️ Optional Yes ⚠️ Optional ⚠️ Optional
Custom subdomains ⚠️ Plan-based ⚠️ Limited ⚠️ Limited
Authentication * ⚠️ Plan/config ⚠️ Access config
Session duration Unlimited No timeout Persistent ⚠️ Not stated ⚠️ Not stated
QR sharing
Server control Full ⚠️ Limited ⚠️ Cloudflare-managed
  • w-share supports Basic Auth per tunnel.

w-share is best when you want a small tunnel server under your own control. Hosted tools are usually easier to start with, but depend on third-party infrastructure and account limits.

Table of contents

Client

Install

The release installer downloads the matching macOS or Linux binary for your machine and verifies it with checksums-sha256.txt. It installs to ~/.local/bin/w-share by default.

# macOS and Linux: installs as ~/.local/bin/w-share
curl -fsSL https://raw.githubusercontent.com/eznix86/w-share/main/install.sh | sh

Pin a specific release by passing the tag explicitly.

curl -fsSL https://raw.githubusercontent.com/eznix86/w-share/main/install.sh | sh -s -- v1.0.1-alpha.0

Override the install directory with W_INSTALL_DIR if you want a different target path.

curl -fsSL https://raw.githubusercontent.com/eznix86/w-share/main/install.sh | W_INSTALL_DIR=/usr/local/bin sh -s -- v1.0.1-alpha.0

Override the installed command name with W_BINARY_NAME if needed.

curl -fsSL https://raw.githubusercontent.com/eznix86/w-share/main/install.sh | W_BINARY_NAME=my-w-share sh

If ~/.local/bin is not already in your PATH, add it in your shell profile:

export PATH="$HOME/.local/bin:$PATH"

Expose a local target

w-share http :8000
w-share http https://awesome-local-website.localhost
w-share share :8000 --name docs
w-share share :8000 --qr
w-share share :8000 --auth

The client prompts for the server URL and shared token on first run and stores them in ~/.config/w-share/config.json.

Use --qr to print a terminal QR code for the assigned public URL. Use --name to request a custom public subdomain like docs.share.domain.tld. Use --auth to protect the public URL with Basic Auth. Pass username:password inline or omit the value to be prompted.

The prompt form is recommended so credentials do not end up in your shell history.

To update the saved client configuration later:

w-share config

Print the installed version for scripts with:

w-share version --json
w-share version --text

Update the installed CLI with the latest release:

w-share update

Install a specific release with:

w-share update v1.0.4

Uninstall the CLI, installed completions, and legacy w binary if it belongs to this project:

w-share uninstall

Also remove saved client configuration with:

w-share uninstall --purge

Shell completions

The installer adds completions automatically for detected bash, zsh, and fish setups. Disable this with W_INSTALL_COMPLETIONS=0 if you prefer to manage completions yourself.

Generate completions manually with:

w-share completion bash
w-share completion zsh
w-share completion fish

Install them with:

# bash
mkdir -p ~/.local/share/bash-completion/completions
w-share completion bash > ~/.local/share/bash-completion/completions/w-share

# zsh
mkdir -p ~/.zsh/completions
w-share completion zsh > ~/.zsh/completions/_w-share

# fish
mkdir -p ~/.config/fish/completions
w-share completion fish > ~/.config/fish/completions/w-share.fish

Server

Run directly

W_SHARE_TOKEN=dev-token w-share serve --domain share.domain.tld --port 8080

Generate a token with:

openssl rand -hex 24

Docker Compose

services:
  w-share:
    image: ghcr.io/eznix86/w:1.0.0-alpha.1
    restart: unless-stopped
    command:
      - serve
      - --domain
      - share.domain.tld
      - --port
      - "8080"
    environment:
      W_SHARE_TOKEN: your-shared-token
    ports:
      - "8080:8080"

Replace:

  • share.domain.tld with your public tunnel domain
  • your-shared-token with the token used by your clients

Kubernetes

Use ghcr.io/eznix86/w:<tag> in your Deployment and pass the server args to the container:

containers:
  - name: w-share
    image: ghcr.io/eznix86/w:1.0.0-alpha.1
    imagePullPolicy: IfNotPresent
    args:
      - serve
      - --domain
      - share.domain.tld
      - --port
      - "8080"
    env:
      - name: W_SHARE_TOKEN
        valueFrom:
          secretKeyRef:
            name: w-share-auth
            key: token

Your Ingress should route both the root domain and wildcard subdomains to the same service:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: w-share
spec:
  ingressClassName: nginx
  rules:
    - host: share.domain.tld
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: w-share
                port:
                  number: 80
    - host: '*.share.domain.tld'
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: w-share
                port:
                  number: 80

Replace:

  • share.domain.tld with your public tunnel domain
  • w-share with your Service name if different

If you use cert-manager, request a certificate for both the root domain and wildcard subdomain:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: w-share-tls
spec:
  secretName: w-share-tls
  issuerRef:
    name: your-cluster-issuer
    kind: ClusterIssuer
  dnsNames:
    - share.domain.tld
    - '*.share.domain.tld'

License

MIT