When working with code locally on your machine there are often times you need a public URL pointing to your local machine (such as webhooks). You could easily just open up the internet on your router and allow access to your local machine port but this is complicated, time consuming, and if you forget to close the port after you are done it can be a security risk. Also what if you bring your laptop to a coffee shop or into the office? You would then have to redo all that work. In this post we are going to cover how to setup your own proxy server to solve this problem (and optionally add ssl encryption to the endpoint if it's a webserver).

I've been using localhost.run lately to accomplish this but I got pretty annoyed by my public URL changing every 24 hours (or even randomly). For the project I was working on I had to constantly go and update my webhook URL to the new sub-domain I was issued. This wastes precious development time so I wanted to find a way to do it myself using my own Home Unraid Server.

Why SSH?

localhost.run (and others) let you use SSH to proxy whereas if you use Ngrok you need to install that separately. I really like the idea of using SSH as it comes on nearly every system out there so you don't need to install it yourself. I also like the idea that I can use this to proxy other applications besides just HTTP/HTTPS requests (such as game servers) which is really handy for me.

SSH makes setting up a proxy very easy. Here is an example of localhost.run that proxies local port 8000 to remote port 80 (when you run this they will tell you what sub-domain you got issued in the terminal output):

ssh -R 80:localhost:8000 ssh.localhost.run

This command is super simple and works on nearly every device. This is what we are trying to replicate in our own self-hosted environment.

VPN or not to VPN

If you are setting this up on your home server or other secure network I highly recommend setting up a VPN server that lets you access the network securely from the outside. This way you don't need to expose SSH directly to the public internet and can instead VPN into your home/server network and access it from there.

I also highly recommend using the strongest encryption method possible along with very very long randomized usernames and passwords for your VPN server. You never want someone inside your private network.

I have the EdgeRouter X and was able to follow this guide to easily setup a VPN server on this router.

Setting it up

DNS Config

You will need to figure out what domain you want to setup to use. I ended up just creating a random sub-domain (i.e. dev.example.com or dev.local.example.com). Figure out what sub-domain or domain you want to use and then edit the DNS records to point to the server that will be proxying the requests (in my case this is my Home Server so I set it to my Home IP address).

You should also figure out what port you are going to proxy to. If you are going to use Nginx/Letsencrypt you need to specify a port that isn't 80 or 443 as Nginx will be using these. For reference I chose port 55555 as it was unused (if you use a different port just replace 55555 in this tutorial with whatever port you are using).

Nginx & Letsencrypt (not required unless you need SSL)

You don't need to setup a NGINX proxy on your server for this to work but it is recommended as you can set it up along with Letsencrypt so that your exposed URL is using HTTPS instead of plain HTTP.

If you are on Unraid you can just install the Letsencrypt image from Linuxserver.io under community applications. I'm not going to go into how to install Letsencrypt on Unraid as it has already been explained a million times (so just go to the Unraid forums and look it up).

Now add a site-conf to Nginx to proxy the port like so (replacing the domain and port with yours):

# redirect non-ssl to ssl
server {
	listen 80;
	server_name dev.example.com;
	return 301 https://$host$request_uri;
}

server {
	listen 443 ssl;

	root /config/www;
	index index.html index.htm index.php;

	server_name dev.example.com;

	ssl_certificate /config/keys/letsencrypt/fullchain.pem;
	ssl_certificate_key /config/keys/letsencrypt/privkey.pem;
	ssl_dhparam /config/nginx/dhparams.pem;
	ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
	ssl_prefer_server_ciphers on;

	# CloudFlare IPs (comment out if you don't use CloudFlare)
	set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/12;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2c0f:f248::/32;
    set_real_ip_from 2a06:98c0::/29;
    
    # use any of the following two
    real_ip_header CF-Connecting-IP;
    #real_ip_header X-Forwarded-For;

	location / {
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header HTTPS on;
        proxy_pass http://192.168.1.10:55555; # my home server IP is 192.168.1.10, replace with your IP and Port.
		#max_body_size will allow you to upload a large git repository
		client_max_body_size 200M;
	}
}

Restart NGINX after adding the above file so it gets loaded.

Enabling GatewayPorts for your SSH server

Normally when ports are forwarded over SSH only the host server will be able to connect to the port. This is a security issue to prevent outsiders from accessing your internally forwarded ports. We need to change the GatewayPorts option to either yes or clientspecified to get around this. Edit your ssh_config file on the SSH server (Unraid this is /boot/config/ssh/sshd_config whereas on Ubuntu this is /etc/ssh/ssh_config) to make this change. This is a server change only and does not need to be modified on the client.

Setting GatewayPorts to clientspecified means the client can specify what hosts are allowed to connect when making the SSH connection like so:

ssh -R 69.120.212.132:8080:localhost:80 host147.example.com

In this example only 69.120.212.132 can access port 8080 on the remote host host147.example.com.

If you set GatewayPorts to yes then all forwarded ports will be accessible from any host. I personally chose this because my server is behind a firewall anyways.

If you are using a proxy server on the same host you are forwarding ports to (and they are not running in separate containers) you can skip this step since your proxy will already have access to the loopback interface. In my case I was running my proxy server in a docker container so I had to enable GatewayPorts to allow my proxy server to connect to my forwarded port.

Connecting & Testing

Now that everything is setup we can now forward a port from our local machine to our server. To do this you will obviously need some sort of web server running on your local machine. I do web development for my primary job and mainly work in the Symfony PHP framework. When working on a project all I do is type this to start a little development webserver on my local machine:

symfony serve

And now I have a local server running on port 8000 (I can type https://localhost:8000 into my web browser and access it). We now run this command to forward my local port 8000 to port 55555 on the server:

ssh -R 55555:localhost:8000 [email protected]

And now traffic hitting the server on port 55555 will be redirected to our local machine. Not only that though because we also have NGINX with Letsencrypt proxying port 55555 we can now access the domain with SSL at https://local-dev.example.com

And that is it! You can now proxy any local port to your remote server to show clients your work, play around with webhooks, or whatever other reason you can think of.

Conclusion

I hope others found this handy as it has been extremely helpful for myself. I used to pay a company for this service but then that company ended up going bankrupt and I didn't have anything that did the same thing for a while. I then found localhost.run which was pretty nice but again had limitations that just got in the way. I'm glad I found a method for accomplishing this without needing to install any extra software on the client or server.

This has been a really rough year for me so if you found this helpful please consider donating or using my referral links below. Every donation helps a ton. If you cannot donate leaving a comment with your gratitude means a lot to me as well and actually helps motivate me to continue writing. You can also subscribe with your email to get notified of new posts.