I worked a bit on a way to launch a Streamlit application as a Passenger app. A bit in the spirit Cunningham’s Law, I figured I would post what I got to work if anyone else in the future was interested, or at least maybe they would post a better way to do this and correct me, but I didn’t want to waste the work.
We don’t have a use case for this application at the moment, so I don’t want to post it to the appverse having only tested it personally without deploying it for anyone, so if someone else is motivated to use this as a starting point and publish it, be my guest.
Implementation
The main thing that makes Streamlit different from a Flask based Passenger app is that Streamlit supports only listening on a TCP/UDS socket or (recently) ASGI protocols, none of these options are available to the PUN, I tried a2wgsi which is supposed to allow you to use ASGI applications (streamlit) with WSGI web servers (Passenger) but I had difficulting using it with the websockets that Streamlit needed.
What I did to get it working was, spawn a NodeJS shim to do the following:
a) spawn the Streamlit process to listen on a unix socket
b) Act as a HTTP proxy to the Streamlit app’s unix socket.
const { createProxyServer } = require('httpxy');
const http = require('http');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
async function startApp() {
// Define a unique path for the socket file
const username = os.userInfo().username
const socketPath = path.join("/tmp", `streamlit_chatbot_${username}.sock`);
// Cleanup function to remove the socket file on exit
const cleanup = () => {
if (fs.existsSync(socketPath)) {
try { fs.unlinkSync(socketPath); } catch (e) { }
}
};
process.on('exit', cleanup);
process.on('SIGINT', () => { process.exit(); });
process.on('SIGTERM', () => { process.exit(); });
// Spawn streamlit app
const streamlit = spawn('bin/python', [
'-m', 'streamlit', 'run', '<path/to/streamlit_app>.py',
'--server.address', `unix://${socketPath}`,
'--server.headless', 'true',
'--browser.gatherUsageStats', 'false',
'--server.baseUrlPath', process.env.PASSENGER_BASE_URI || '/'
]);
// Awful hard coded sleep to wait for the streamlit app to start
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await sleep(2000);
// Logging
streamlit.stdout.on('data', (data) => console.log(`[Streamlit] ${data}`));
streamlit.stderr.on('data', (data) => console.error(`[Streamlit Error] ${data}`));
const proxy = createProxyServer({
target: {
socketPath: socketPath
},
ws: true
});
const server = http.createServer(async (req, res) => {
try {
await proxy.web(req, res, {
target: socketPath,
});
}
catch (err) {
console.error('[Proxy Error]', err);
res.statusCode = 502;
res.end('Bad Gateway: Streamlit is starting or socket is unavailable.');
}
});
// Websocket handling
server.on("upgrade", (req, socket, head) => {
proxy.ws(req, socket, { target: { socketPath: socketPath } }, head);
});
server.listen(0, () => {
console.log(`Proxying to Streamlit via Unix Socket: ${socketPath}`);
});
}
startApp().catch(console.error);
Other than this file is a standard Passenger app, you would have app.js and node_modules to start the shim, and the dependencies/code for your Streamlit app in the same directory. But technically the application is a NodeJS passenger app not a Python passenger app so you would not have a passenger_wsgi.py file.
Hope somebody finds this useful.