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.