Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SSH Reverse Tunnel

Overview

An SSH reverse tunnel exposes a local service to the internet through a VPS. It works by establishing an outbound SSH connection from your local machine to the VPS, which then forwards incoming traffic back through that connection to your local service.

This is useful when you are behind NAT, a firewall, or lack a public IP address.

Architecture

Internet Request
       │
       ▼
┌─────────────────┐
│  VPS (Public)   │
│  Caddy :443     │
│       │         │
│       ▼         │
│  localhost:5201 │◄── SSH tunnel listens here
└────────┬────────┘
         │
    SSH Connection
    (outbound from local)
         │
         ▼
┌─────────────────┐
│  Local Machine  │
│  localhost:8080 │◄── Your service
└─────────────────┘

Traffic flow:

  1. Client requests https://<domain>
  2. Caddy terminates TLS and proxies to 127.0.0.1:5201
  3. Port 5201 is the remote end of the SSH tunnel
  4. Traffic flows through the tunnel to your local machine on port 8080

Prerequisites

  • VPS setup completed (see VPS Setup)
  • Caddy running in Docker (see Caddy Setup)
  • A local service running (this guide uses localhost:8080)

Setup

Add a reverse proxy block to your Caddyfile for the subdomain you want to expose. Caddy will automatically handle HTTPS:

tunnel.<domain> {
    reverse_proxy localhost:5201
}

Restart Caddy to apply the change:

docker compose restart caddy

SSH Reverse Tunnel Command

From your local machine:

ssh -N -R 5201:localhost:8080 <username>@<vps-ip>
FlagPurpose
-NDo not execute a remote command. Port forwarding only.
-R 5201:localhost:8080Bind remote port 5201 to local port 8080.

Format: -R [remote_port]:[local_host]:[local_port]

The tunnel remains open while the SSH connection is active.

Docker Networking Note

If Caddy is running in a Docker container (non-host network), it may not be able to reach 127.0.0.1:5201 on the host. To fix this, either:

  1. Add GatewayPorts to the VPS SSH server config (/etc/ssh/sshd_config):

    GatewayPorts clientspecified
    

    Then use 0.0.0.0:5201 in your tunnel command:

    ssh -N -R 0.0.0.0:5201:localhost:8080 <username>@<vps-ip>
    
  2. Use host network mode for Caddy (not recommended for production).

Observability

Stream Caddy logs to your local machine:

ssh <username>@<vps-ip> "docker logs -f caddy-caddy-1" | grep --line-buffered <domain>

Example

# Start local service
npm run dev  # localhost:3000

# Establish tunnel
ssh -N -R 5201:localhost:3000 <username>@<vps-ip>

Access from anywhere: https://<domain>