How to Run a Streamlit App in a Cloud Sandbox With an HTTPS URL
Updated October 27, 2026 .md
Short answer: Install Streamlit in a venv, run it as a systemd service on port 8501, then run sbx host
What we ran#
Everything below ran on prod on 2026-10-27, on one small sandbox (2 vCPU, 4 GB) with Python 3.12.3 and Streamlit 1.66.1. Tokens and cookies are replaced with <token> and <cookie>. For how hosted URLs, tokens and cookies work in general, see Give a sandbox port an HTTPS URL. This page covers what is specific to Streamlit.
| Step | Result |
|---|---|
pip install streamlit in a fresh venv |
18.5 s |
Service start to /_stcore/health answering ok |
0.62 s |
| App drawn in headless Chromium through the private URL | 2.0 s from the first request |
| Request or WebSocket without the token | 401 |
| File upload with CORS and XSRF protection left on | Worked |
Browser tab left open across sbx stop --wait and sbx resume |
Reconnected, st.session_state kept |
Process killed with kill -9 |
systemd restarted it, healthy again in 0.87 s |
Why Streamlit needs a WebSocket-capable proxy#
A Streamlit page is almost empty HTML. The app itself arrives over one WebSocket at /_stcore/stream: every widget, every rerun and every chart goes through it. A proxy that serves the HTML but drops the WebSocket upgrade gives you a page that spins forever. Sandboxer's hosting proxy passes WebSockets through (Hosting), so nothing needs to change in Streamlit.
The other endpoints that matter:
| Path | What it does | Through a private hosted URL |
|---|---|---|
/ |
HTML shell | 401 without the token, 200 with the cookie |
/_stcore/stream |
The app's WebSocket | 401 without the cookie, 101 with it |
/_stcore/health |
Returns ok when the server is up |
401 without the cookie, ok with it |
/_stcore/upload_file/… |
st.file_uploader uploads, checked against the _streamlit_xsrf cookie |
Worked from the browser |
1. Install Streamlit#
The Streamlit install docs use a virtual environment and pip install streamlit. The sandbox already has Python 3.12:
sbx new --name streamlit --type small
sbx exec sb_emnyl4g91w --timeout 600 "mkdir -p ~/st-app && cd ~/st-app && python3 -m venv .venv && time .venv/bin/pip install -q streamlit; .venv/bin/streamlit version"real 0m18.533s
…
Streamlit, version 1.66.1sbx exec stops after 30 seconds by default, so give installs a --timeout.
2. A test app#
This app exercises the parts that break behind a bad proxy: a button that changes st.session_state (WebSocket round trips), a cached function, and a file upload (XSRF check). Save it as ~/st-app/app.py in the sandbox:
import time
import pandas as pd
import streamlit as st
st.title("Hello from a sandbox")
if "clicks" not in st.session_state:
st.session_state.clicks = 0
if st.button("Click me"):
st.session_state.clicks += 1
st.write(f"Clicks this session: {st.session_state.clicks}")
@st.cache_data
def slow_square(n: int) -> int:
time.sleep(2)
return n * n
t = time.perf_counter()
v = slow_square(12)
st.write(f"slow_square(12) = {v} in {time.perf_counter() - t:.2f} s")
up = st.file_uploader("Upload a CSV", type="csv")
if up is not None:
df = pd.read_csv(up)
st.write(f"Uploaded {up.name}: {len(df)} rows, columns {list(df.columns)}")3. Run it as a service#
Running streamlit run from sbx exec --detach works, but a systemd unit restarts it if it crashes and starts it again when the sandbox boots from disk:
sbx exec sb_emnyl4g91w 'sudo tee /etc/systemd/system/streamlit.service >/dev/null <<EOF
[Unit]
Description=Streamlit app
After=network-online.target
[Service]
User=user
WorkingDirectory=/home/user/st-app
ExecStart=/home/user/st-app/.venv/bin/streamlit run app.py --server.port 8501 --server.headless true
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now streamlit'--server.headless true stops Streamlit from trying to open a browser or ask for an email on first run. You don't need --server.address 0.0.0.0: with no address set, Streamlit already listens on every interface, so the hosting proxy can reach it without --localhost:
sbx exec sb_emnyl4g91w "curl -s localhost:8501/_stcore/health; echo; ss -ltn | grep 8501"ok
LISTEN 0 2048 *:8501 *:*From systemctl start to the health endpoint answering took 0.62 seconds. Use /_stcore/health as your readiness check in scripts. / only serves the static HTML shell.
4. Give it a private URL#
sbx host sb_emnyl4g91w 8501 --title Streamlithttps://leafy-lunar-bugle-8501.box.sandboxer.com?_token=<token>Without the token, the proxy answers and Streamlit never sees the request. That includes the WebSocket:
curl -si https://leafy-lunar-bugle-8501.box.sandboxer.com/ | head -1
curl -s -o /dev/null -w "%{http_code}\n" https://leafy-lunar-bugle-8501.box.sandboxer.com/_stcore/healthHTTP/2 401
401The token link returns a 302 that sets the 30-day __sbx_auth cookie. With that cookie, the WebSocket handshake to /_stcore/stream returned 101 Switching Protocols, and without it 401. A browser sends the cookie on its own, so opening the token link is all a viewer has to do.
We opened the link in headless Chromium (Playwright 1.64.1), clicked the button three times, reloaded, and uploaded a three-row CSV:
first response: 200
app rendered after 2004 ms
after 3 clicks: Clicks this session: 3
after reload: Clicks this session: 0 | slow_square(12) = 144 in 0.11 s
Uploaded temps.csv: 3 rows, columns ['city', 'temp']
cookies: __sbx_auth, ajs_anonymous_id, _streamlit_xsrf
websocket: wss://leafy-lunar-bugle-8501.box.sandboxer.com/_stcore/stream frames received: 18
websocket: wss://leafy-lunar-bugle-8501.box.sandboxer.com/_stcore/stream frames received: 23A reload starts a new session, so st.session_state goes back to 0, while st.cache_data is shared by the server process, so the 2-second function came back in 0.11 seconds. ajs_anonymous_id comes from Streamlit's usage statistics; turn it off with browser.gatherUsageStats = false (config options).
Leave CORS and XSRF protection on#
Answers for running Streamlit behind a proxy often say to start it with --server.enableCORS false --server.enableXsrfProtection false. On a hosted URL you don't need either:
- The page, the WebSocket and the upload all come from the same origin (
https://<subdomain>-8501.box.sandboxer.com). Streamlit's origin check passes and the_streamlit_xsrfcookie matches, so the upload above worked with both protections on. - The origin check is doing real work. Inside the sandbox, a WebSocket handshake with a foreign
Originheader got403from Streamlit, while the same handshake with the hosted URL asOrigingot101:
# inside the sandbox, straight to Streamlit
Origin https://evil.example -> 403
Origin https://leafy-lunar-bugle-8501.box.sandboxer.com -> 101Streamlit's config docs also note that the two settings conflict, and that XSRF protection takes priority if it is on while CORS is off. If an upload fails with a 403, check that you opened the app on its hosted URL and not through a second proxy or an iframe on another domain.
Public or private?#
| Private (default) | sbx host <id> 8501 --public |
|
|---|---|---|
| Who can open it | Anyone with the token link or its cookie | Anyone |
| WebSocket needs | The __sbx_auth cookie, which the browser has after the token link |
Nothing |
| Use it for | Internal tools, apps that read your data or secrets | Demos with no sensitive data |
Streamlit has no login of its own in this setup. A public URL runs your app, with its database credentials and API keys, for any visitor. Switching public and back to private mints a new token, which cuts off old links.
Stop, resume and crashes#
We left the browser tab open (after clicking twice), stopped the sandbox, waited 20 seconds, and resumed it:
sbx stop sb_emnyl4g91w --wait # 2.2 s; the URL returned 404 while stopped
sbx resume sb_emnyl4g91w # 1.3 s, memoryRestored: truebefore stop: Clicks this session: 2
after resume + 1 click: Clicks this session: 3 (1528 ms)The Streamlit process was the same one (MainPID=673 before and after). The open tab's WebSocket reconnected to it, and the session kept its state, so the next click counted to 3. Memory comes back only when the resume lands on a machine with the same CPU type. Otherwise the sandbox boots from disk (memoryRestored: false): the venv, the app and the hosted route are kept, and the enabled unit starts Streamlit again, but sessions and st.cache_data start empty. We didn't force a disk-only boot in this test.
Crash recovery is quick. After sudo kill -9 on the Streamlit process, systemd started a new one (NRestarts=1) and /_stcore/health answered again 0.87 seconds later. A new process has no sessions, so state from before the crash is gone.
FAQ#
Why does my Streamlit page spin forever?#
The HTML loaded but the /_stcore/stream WebSocket did not connect. On a private URL that happens when the handshake has no __sbx_auth cookie, for example in a script that sent ?_token= on the WebSocket URL. Open the token link first, or see the cookie example in Give a sandbox port an HTTPS URL.
Do I need --server.address 0.0.0.0?#
No. With no address set, Streamlit 1.66.1 listened on all interfaces (*:8501). If you set server.address to 127.0.0.1, host the port with --localhost.
Should I turn off enableCORS or enableXsrfProtection?#
No. The hosted URL serves everything from one origin, so uploads and the WebSocket work with both on. Turning them off removes Streamlit's check against other sites driving your app.
Which size do I need?#
The test app ran on small (2 vCPU, 4 GB) with most of the memory free. Pick default (4 vCPU, 8 GB) if your app loads large dataframes or models.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.