Skip to content
Guides · Recipes

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 8501. The private URL's token gates the page and the /_stcore/stream WebSocket. Keep Streamlit's CORS and XSRF protection on: uploads and reruns worked through the proxy with the default settings.

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.1

sbx 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 Streamlit
https://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/health
HTTP/2 401
401

The 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: 23

A 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_xsrf cookie 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 Origin header got 403 from Streamlit, while the same handshake with the hosted URL as Origin got 101:
# inside the sandbox, straight to Streamlit
Origin https://evil.example -> 403
Origin https://leafy-lunar-bugle-8501.box.sandboxer.com -> 101

Streamlit'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: true
before 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.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

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.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

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.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

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.

Read the guide
Best E2B alternatives in 2026, ranked

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.

Read the guide