I wrote two web applications for personal use. The backend for both apps was written in Python, a combination of Pandas and Flask. To expose the service, the backend was run on a specific port.
As for the UI, browser consumes the APIs through JS.
This was when annoyance struck: the API endpoints were designed to be exposed only via localhost
for security, therefore necessitated SSH tunneling in its early design.
This was not so much of an issue since I exposed the ports through a WireGuard VPN interface.
However, for certain situations where I could not use WireGuard, I was left with SSH tunneling.
Then I realized that it would be a hassle needing to expose 2 or more ports at once.
I wrote a bash
function to alleviate the pain, but that was only treating the symptom.
I needed a more permanent solution.
# Function
function tunnel() { ssh -L ${1}:localhost:${1} {$2} -NC }
# Establishing a tunnel
tunnel 8081 vps
Gunicorn is a WSGI HTTP server.
Most tutorials would show how to bind a Gunicorn server on a port.
The line below assumes there is a Flask application object app
inside the api.py
file, hence api:app
directive.
gunicorn --workers=2 --bind=127.0.0.1:8081 api:app
But recently, I learned that Gunicorn can listen on a Unix domain socket.
gunicorn --workers=2 --bind unix:app.sock -m 007 api:app
The -m 007
parameter sets the app.sock
accessible only to the current $USER
.
Now, the next part is accessing app.sock
through a reverse proxy.
Nginx can be used for reverse-proxying, but I chose Caddy because of its portability.
A Caddyfile local to the project directory is as follows:
:8081 {
root * webroot
file_server
handle_path /api/* {
reverse_proxy * unix/app.sock
}
}
Issuing caddy run
in this application directory runs a Caddy server on port :8081
, serving the frontend UI webroot
, accessible at 0.0.0.0:8081
.
The API backend is accessible at 0.0.0.0:8081/api
, thanks to the handle_path
directive that reverse-proxies to app.sock
socket.
Figuring this out took a while, but when it worked it felt like magic.