Andrey Yatsyk

Self-Hosted Ngrok Alternative

Self-Hosted Ngrok Alternative

What is ngrok and Why Do We Need It?

ngrok is a reverse proxy that creates a secure tunnel from a public endpoint to a local service. It allows developers to expose a local server behind a NAT or firewall to the internet without deploying to a public hosting environment. This is particularly useful for testing purposes or sharing work-in-progress applications.

There are indeed easier ways to self-host ngrok-like services—various resources provide examples on how to set these up. However, these methods can conflict with existing configurations. In my case, I use Traefik to serve my Docker services, and many of these self-hosted tunneling solutions could interfere with this setup. Therefore, I needed a solution that integrates seamlessly with Traefik without causing conflicts.

Requirements and Setup

To create a self-hosted alternative to ngrok, we'll leverage:

  • Traefik: A HTTP reverse proxy and load balancer that makes deploying microservices easy.
  • Docker: Containerization platform to package our applications.
  • SSH: Secure Shell protocol to create a tunnel to our Virtual Private Server (VPS).
  • socat: A command-line based utility that establishes two bidirectional byte streams and transfers data between them.

Our goal is to:

  • Integrate the self-hosted service into our existing VPS infrastructure.
  • Use Traefik for SSL termination.
  • Avoid running additional web servers.
  • Start new tunnels only from the client machine.
  • Use Docker to start a new socat proxy container.
  • Establish an SSH connection to the VPS without executing commands on it.

Using socat as a TCP Proxy

socat (SOcket CAT) is a networking tool that allows for data transfer between two bidirectional data streams. It's like a combination of netcat and cat. We use socat within a Docker container to create a TCP proxy that Traefik can route to, and because we need to attach labels to the Docker container for Traefik's dynamic configuration.

Docker Compose Configuration

Below is the docker-compose.yml file for the socat proxy. Replace placeholders like ${DOMAIN} with your actual domain and adjust labels as necessary.

version: "3.3"
 
services:
  developer-container-proxy:
    container_name: developer-container-proxy
    image: alpine/socat
    restart: unless-stopped
    command: "TCP4-LISTEN:7004,fork TCP4:host.docker.internal:7003"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    labels:
      - "traefik.http.routers.developer-container-proxy.rule=Host(`developer.${DOMAIN}`)"
      - "traefik.http.routers.developer-container-proxy.entrypoints=websecure"
      - "traefik.http.routers.developer-container-proxy.tls.certresolver=myresolver"
      - "traefik.http.services.developer-container-proxy.loadbalancer.server.port=7004"

Traefik Configuration

Here's the docker-compose.yml file for Traefik:

version: "3.7"
 
services:
  traefik:
    image: "traefik:v2.9"
    container_name: traefik
    restart: unless-stopped
    command:
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --providers.docker
      - --api
      - --certificatesresolvers.myresolver.acme.email=youremail@example.com
      - --certificatesresolvers.myresolver.acme.storage=/config/acme.json
      - --certificatesresolvers.myresolver.acme.tlschallenge=true
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./config/:/config"
    labels:
 
      # Dashboard
      - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls.certresolver=myresolver"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.middlewares=authtraefik"
      - "traefik.http.middlewares.authtraefik.basicauth.users=${USER_PASSWORD}" # user:password
 
      # Global redirect to HTTPS
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
 

Ensure that ${DOMAIN} and ${USER_PASSWORD} are defined in your environment or replace them directly.

Setting Up the SSH Tunnel

To forward traffic from your local machine to the VPS, use the following SSH command:

ssh -R "*:7003:localhost:7003" your-vps-domain.com

Explanation:

  • -R "*:7003:localhost:7003": Remote port forwarding, binding to all interfaces on the server (*).
  • your-vps-domain.com: Replace with your VPS domain or IP address.

SSHD Configuration for GatewayPorts

By default, SSHD binds remote port forwarding to localhost, which isn't accessible from Docker containers. To allow binding to all interfaces, adjust the SSHD configuration on your VPS:

  1. Open the SSHD configuration file, typically located at /etc/ssh/sshd_config.

  2. Set the GatewayPorts option to clientspecified:

GatewayPorts clientspecified

Restart the SSHD service:

sudo systemctl restart sshd

This configuration allows the SSH client to specify the bind address, enabling Docker containers to access the forwarded port.

Resources

On this page