Streamlit is the fastest way to turn a data script into a shareable web app — dashboards, model demos, internal tools. The catch when deploying is that Streamlit isn't a static site or a stateless API: it's a stateful server that holds a WebSocket connection per user session. Get the deployment details right and it just works.
This guide containerizes a Streamlit app, wires it to a managed database, and deploys it.
What makes Streamlit different to deploy
- Stateful sessions. Each browser tab is a session held over WebSocket. You can't blindly load-balance across replicas without sticky sessions.
- Single long-lived server. It runs
streamlit run, not a build-and-serve-static flow. - Re-runs top to bottom. Every interaction re-executes the script, so expensive work must be cached.
Because of session state, the simplest robust deployment is a single replica (or sticky-session routing if you scale out). For most internal data apps, one well-sized replica is plenty.
Step 1: Cache expensive work
Before deploying, make sure your app caches data loads and model inits — otherwise every widget click re-queries your database.
import streamlit as st
import pandas as pd
import os
from sqlalchemy import create_engine
@st.cache_resource
def get_engine():
return create_engine(os.environ["DATABASE_URL"], pool_pre_ping=True)
@st.cache_data(ttl=300)
def load_sales():
return pd.read_sql("SELECT * FROM sales", get_engine())
df = load_sales()
st.title("Sales Dashboard")
st.metric("Total", f"${df.revenue.sum():,.0f}")
st.bar_chart(df.groupby("region").revenue.sum())@st.cache_resource for connections/models; @st.cache_data for serializable data with a TTL.
Step 2: Containerize
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", \
"--server.port=8501", \
"--server.address=0.0.0.0", \
"--server.headless=true", \
"--browser.gatherUsageStats=false"]# requirements.txt
streamlit==1.39.0
pandas==2.2.3
sqlalchemy==2.0.35
psycopg2-binary==2.9.9The critical flags:
--server.address=0.0.0.0so the platform ingress can reach it.--server.headless=trueso it doesn't try to open a browser.--server.port=8501(Streamlit's default).
Step 3: Provision a managed database
Most data apps read from a database. Create a managed PostgreSQL (14.x or 16.x) on [PandaStack](https://dashboard.pandastack.io) and link it to the app — DATABASE_URL is injected automatically, which the SQLAlchemy engine above reads directly. No connection-string juggling.
Use pool_pre_ping=True (shown earlier) so stale connections after idle periods are recycled cleanly.
Step 4: Deploy
- 1Push the repo to GitHub.
- 2Create a container app and connect the repo — BuildKit builds the Dockerfile and Helm deploys it.
- 3Link the managed PostgreSQL database.
- 4Expose port
8501. - 5Set the health check to
/_stcore/health(Streamlit's built-in health endpoint). - 6Add a custom domain; SSL is automatic.
Step 5: Handle WebSockets and scaling
Streamlit needs WebSocket upgrades to pass through the ingress — PandaStack's Kong ingress handles WebSocket connections, so live widgets work out of the box.
On scaling:
| Strategy | When |
|---|---|
| Single replica | Default for internal data apps — simplest, no session issues |
| Multiple replicas + sticky sessions | High-traffic public dashboards |
| Scale-to-zero (free tier) | Low-traffic internal tools where a cold start is acceptable |
For a rarely used internal dashboard, free-tier scale-to-zero is a great fit — it costs nothing when idle and spins up on the next visit (with a short cold start). For an always-watched dashboard, keep one warm replica.
Step 6: Add authentication
Streamlit has no built-in auth for arbitrary deployments. Protect internal apps with:
- PandaStack team/org access controls and SSO.
- A reverse-proxy auth layer (e.g. Cloudflare Access) on the custom domain.
- Streamlit's native authentication features for OIDC providers if you wire them up.
Don't leave a dashboard that queries production data open to the internet.
Common pitfalls
- Forgetting caching — leads to a database query storm on every interaction.
- Local file writes — containers are ephemeral; write to object storage or the database.
- Wrong health check — use
/_stcore/health, not/. - Blocking the main thread — long computations freeze the UI; cache or offload them.
References
- [Streamlit documentation](https://docs.streamlit.io/)
- [Streamlit caching](https://docs.streamlit.io/develop/concepts/architecture/caching)
- [Streamlit Docker deployment](https://docs.streamlit.io/deploy/tutorials/docker)
- [Streamlit configuration options](https://docs.streamlit.io/develop/api-reference/configuration/config.toml)
---
Streamlit is a joy once the stateful-server details are handled — caching, WebSockets, and a single sized replica. PandaStack auto-wires a managed PostgreSQL via DATABASE_URL and its Kong ingress passes WebSockets through cleanly. Deploy your first data app free at [dashboard.pandastack.io](https://dashboard.pandastack.io).