Skip to content

“self-hosted ngrok”: tunnel to localhost with SSH reverse tunneling + Nginx + Letsencrypt

Recently we come across a use case that we need to proxy localhost to a https public domain and found ngrok to be very intuitive and easy to use.

For example, I have a local dev server running on http://localhost:3000 and running ngrok gives me a public address with SSL supported

However for the free tier it doesn’t support sub-domain reservation. Each time I start ngrok again it gives me a new address, which makes it difficult to use. So I decide to figure out ways to run this reverse tunneling on my own VPS server on Linode.

Public tutorials on this has been very complete and helpful, but I still take this note down for my specific setup case for future reference and share it out. Maybe your case is very similar to mine and I hope I can bring some approaches here.

This note is majorly based on Alex’s answer at and I will paste other references along the way.

Before setting up everything, get a A record like tunnel.yourdomain on Linode DNS provider.

First we need to get a wildcard SSL certificate for our domain (* Though SSL is not must need to enable the tunneling, it is strongly recommended for security.

To obtain free SSL certificate from Letsencrypt for my Linode Ubuntu instance, I took the instructions (wildcard section) from (Snap is already included in Ubuntu 18.04 LTS). Here are the commands:

$ sudo snap install core; sudo snap refresh core'
$ sudo apt-get remove certbot # remove existing if any
$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
$ sudo snap set certbot trust-plugin-with-root=ok
$ sudo snap install certbot-dns-linode

After this I obtain the wildcard certificate manually. To setup credentials for Linode

$ sudo certbot certonly \
  --dns-linode \
  --dns-linode-credentials ~/.secrets/certbot/linode.ini \
  --dns-linode-propagation-seconds 120 \
  -d *

Collect the path to those certificates. And use Alex’s Nginx config with http to https redirect:

# Alex's
server {
    server_name tunnel.yourdomain;

    access_log /var/log/nginx/$host;

    listen 443 ssl;
    ssl_certificate /path/to/tls/cert/fullchain.pem;
    ssl_certificate_key /path/to/tls/cert/privkey.pem;

    location / {
        proxy_pass http://localhost:3000/;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_redirect off;

    error_page 502 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
# added by me to automatically redirect http to https
server {
    if ($host = tunnel.yourdomain) {
        return 301 https://$host$request_uri;

    listen 80;
    server_name tunnel.yourdomain;
    return 404;

Reload nginx, and when client’s localhost is not available, tunnel.yourdomain will return 404 in my case.

Start localhost:3000 on client and run ssh -R 3000:localhost:3000 yourdomain will start a ssh session with reverse proxy.

Happy rock on https://tunnel.yourdomain !

Leave a Reply

Your email address will not be published. Required fields are marked *