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:
- Client requests
https://<domain> - Caddy terminates TLS and proxies to
127.0.0.1:5201 - Port 5201 is the remote end of the SSH tunnel
- 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>
| Flag | Purpose |
|---|---|
-N | Do not execute a remote command. Port forwarding only. |
-R 5201:localhost:8080 | Bind 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:
-
Add
GatewayPortsto the VPS SSH server config (/etc/ssh/sshd_config):GatewayPorts clientspecifiedThen use
0.0.0.0:5201in your tunnel command:ssh -N -R 0.0.0.0:5201:localhost:8080 <username>@<vps-ip> -
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>