You built a Streamlit app. It works on localhost. You showed it to your manager and they said "this is great, can the whole team use it?"
And then reality sets in. You need: authentication (so not everyone on the internet can see your revenue data), secrets management (so your database password isn't hardcoded), HTTPS, always-on hosting (no "app is sleeping" page when someone visits at 9am), and ideally some way for the dashboard to refresh itself when data changes rather than only when someone clicks the browser reload button.
This is the gap between "I built a cool thing" and "this is a production tool my team relies on." If you've already decided Streamlit is the right tool (maybe after reading our Streamlit vs Grafana comparison), this post covers what comes next: actually deploying it properly.
The 5 deployment options (honest comparison)
Let's cut through the noise. Here are your real options in 2026:
| Option | Auth | Cold starts | Custom domains | Scheduling | Ops burden |
|---|---|---|---|---|---|
| Streamlit Community Cloud | GitHub-gated only | 30-60s | No | No | None |
| Streamlit in Snowflake | Snowflake roles | None | N/A (in-app) | Snowflake Tasks | Low (but locked in) |
| Self-hosted (Docker + nginx) | Whatever you build | None | Yes | Cron jobs | High |
| Cloud Run / ECS / K8s | IAP / ALB rules / custom | Depends on config | Yes | External scheduler | Medium-High |
| Managed platform (e.g. Fastero) | Org SSO inherited | None | Yes | Built-in triggers | Low |
Each of these is the right answer for someone. Let me break down when.
Streamlit Community Cloud is genuinely great for demos, portfolios, and open-source projects. It's free. The cold starts are annoying but tolerable for low-traffic apps. The dealbreaker for most teams: you can't add real authentication. You can restrict access to people with GitHub accounts in your org, but that's it. No SSO, no role-based access, no "only the finance team can see this."
Streamlit in Snowflake makes sense if your entire data stack is already Snowflake. You get Snowflake's native auth and governance. The limitation: your app can only access Snowflake data. Need to call an external API or read from Postgres? You're out of luck.
Self-hosted Docker + nginx gives you full control. This is what I'd recommend if you have a DevOps person on the team and specific compliance requirements. The setup isn't trivial though—especially the WebSocket configuration, which trips up nearly everyone the first time. More on this below.
Cloud Run / ECS / Kubernetes is the "enterprise" self-hosted option. Scalable, production-grade infrastructure. But configuring WebSocket support correctly on these platforms requires careful attention, and you're still building auth and scheduling yourself.
Managed platforms trade control for convenience. You push code, it deploys. Auth comes from your org's identity provider. Scheduling is built in. The trade-off is real: you're dependent on the platform, and you have less control over the underlying infrastructure.
The self-hosted approach (the full picture)
Let's walk through what self-hosting actually looks like. I'm not going to sugarcoat this—it's 2-4 hours of setup if you know what you're doing, and ongoing maintenance after that. But it gives you full control.
The Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
ENTRYPOINT ["streamlit", "run", "app.py", \
"--server.port=8501", \
"--server.address=0.0.0.0", \
"--server.headless=true", \
"--browser.gatherUsageStats=false"]Nothing surprising here. The headless=true flag is important—without it, Streamlit tries to open a browser on startup, which obviously fails in a container.
The nginx config (this is where people get stuck)
Streamlit uses WebSockets for its real-time communication between the browser and the server. If your reverse proxy doesn't handle WebSocket upgrades correctly, you'll get a working page that never actually updates—or worse, an infinite "Connecting..." spinner.
upstream streamlit {
server app:8501;
}
server {
listen 80;
server_name your-app.example.com;
# This is the part most people miss
location / {
proxy_pass http://streamlit;
proxy_http_version 1.1;
# WebSocket upgrade headers — required
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Streamlit's WebSocket connections are long-lived
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
# Streamlit's health endpoint (useful for load balancers)
location /_stcore/health {
proxy_pass http://streamlit/_stcore/health;
}
}The critical lines are proxy_set_header Upgrade $http_upgrade and proxy_set_header Connection "upgrade". Without these, WebSocket connections fail silently—the page loads, but interactive widgets don't work. I've seen teams spend hours debugging this because the error isn't obvious.
The proxy_read_timeout 86400 is also important. Default nginx timeout is 60 seconds, and Streamlit keeps WebSocket connections open indefinitely. Without a long timeout, connections drop and users see the "Connecting..." spinner every minute.
Adding authentication
For self-hosted deployments, you have a few options:
OAuth2 Proxy — sits in front of your app and handles Google/GitHub/Azure AD login. Users authenticate through their identity provider, and only authenticated requests reach your Streamlit app.
Authelia — similar concept, more features (2FA, access control lists, multiple identity providers).
Cloudflare Access — if you're already on Cloudflare, this is probably the easiest option. Zero-trust access without modifying your app.
The pattern is the same for all of them: put an auth proxy in front of nginx, configure your identity provider, and Streamlit never knows authentication exists. Your app code stays clean.
Here's a minimal docker-compose showing the OAuth2 Proxy approach:
services:
app:
build: .
expose:
- "8501"
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:latest
environment:
- OAUTH2_PROXY_PROVIDER=google
- OAUTH2_PROXY_CLIENT_ID=${GOOGLE_CLIENT_ID}
- OAUTH2_PROXY_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- OAUTH2_PROXY_COOKIE_SECRET=${COOKIE_SECRET}
- OAUTH2_PROXY_UPSTREAMS=http://nginx:80
- OAUTH2_PROXY_EMAIL_DOMAINS=yourcompany.com
ports:
- "4180:4180"
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- appThis restricts access to people with @yourcompany.com Google accounts. It works, but you're now maintaining three services (app + nginx + oauth2-proxy), their configurations, and the identity provider setup.
If you have a DevOps person, this is totally manageable. If you're a data team without DevOps support, it's a lot of moving pieces to maintain alongside your actual work.
The managed approach (Fastero)
For comparison, here's what the same deployment looks like on a managed platform. I'll use Fastero since that's what we built, but the general pattern applies to any hosted Streamlit solution.
Deploy: Push your Python code (just app.py and requirements.txt). No Dockerfile, no nginx config, no docker-compose. The platform handles containerization and WebSocket routing.
Auth: Your app inherits your organization's authentication automatically. If your team uses Google Workspace, Okta, or SAML, people sign in the same way they sign into everything else. No OAuth2 Proxy configuration, no identity provider setup.
Secrets: Environment variables configured in the UI, encrypted at rest. Your app reads them with os.environ["DATABASE_URL"] like normal. No .env files in git, no secrets manager to set up.
Always-on: No cold starts. No "app is sleeping" page. The app is running when someone visits it.
The trade-off is real: you don't control the infrastructure. If you need custom networking rules, specific container configurations, or compliance certifications the platform doesn't have, self-hosting is still the right call.
You can see the full feature breakdown on our Hosted Streamlit Apps page.
The scheduling problem (the part most deployments miss)
Here's the thing nobody talks about when deploying Streamlit: the app only refreshes when someone visits it and triggers a rerun.
For interactive tools (upload a file, pick parameters, get results), that's fine. But for dashboards—the kind where someone opens a tab in the morning and expects to see current data—it's a problem.
With a standard deployment, if nobody refreshes the page, the data gets stale. The chart from 8am is still showing at 3pm.
How each option handles this
Community Cloud: You can hack it with st.rerun() inside a time.sleep() loop. This burns compute resources continuously, and your app gets put to sleep after inactivity anyway. Not a real solution.
Self-hosted: Set up a cron job that hits the app URL periodically. Fragile—if the URL changes, the cron breaks. If the app needs authentication, the cron needs credentials. And you're still just forcing a page reload, not a data refresh.
Cloud Run / ECS: Same cron approach, plus you're paying for the container to stay warm waiting for those cron hits.
Fastero: Attach a trigger to the app. Triggers fire on cron schedules, webhooks, or data changes (CDC). When the trigger fires, the app reruns with fresh data. No polling, no cron jobs, no sleep loops.
The trigger-driven model is fundamentally different from "refresh on a timer." Instead of asking "is it time to refresh yet?" every N minutes, the system says "the data changed, refresh now." This means your dashboard updates within seconds of new data arriving, not on whatever schedule you guessed at.
We covered this pattern in depth in the Streamlit vs Grafana post—it's what bridges the gap between Streamlit's on-demand model and Grafana's always-current feel.
Decision framework: who should use what
After watching dozens of teams deploy Streamlit apps, here's my honest recommendation:
Use Community Cloud if: You're building a demo, a portfolio project, or an open-source tool. The audience is public or semi-public. Cold starts are tolerable. You don't need real authentication.
Use Streamlit in Snowflake if: Your data lives entirely in Snowflake, your org already has Snowflake, and you don't need external API calls or non-Snowflake data sources.
Self-host if: You have a DevOps team (or at least one person comfortable with Docker/nginx/auth proxies), you have specific compliance requirements, or you need full control over the infrastructure. Budget 2-4 hours for initial setup and ongoing maintenance time.
Use a managed platform if: You're a data team without dedicated DevOps support, you need production-grade auth and uptime, and you'd rather spend time building apps than configuring infrastructure. Evaluate based on your specific needs—Fastero handles the hosted Streamlit use case, but check what fits your stack.
Consider Cloud Run / ECS if: You already have infrastructure expertise in-house, you need auto-scaling for high-traffic apps, and you're comfortable configuring WebSocket support and building auth yourself.
What this looks like in practice
The teams I see succeed with Streamlit in production follow a pattern:
- Prototype on Community Cloud — get the app working, share it with stakeholders, validate the concept
- Move to production hosting when stakeholders say "we want to use this every day" — that's when auth, uptime, and scheduling matter
- Add triggers when someone asks "why isn't this showing today's data?" — that's when you realize on-demand refresh isn't enough for dashboards
The deployment choice isn't permanent. Start simple, upgrade when the pain justifies it.
If you're evaluating options for your team, our Streamlit alternatives page covers the full landscape, and the Fastero vs Streamlit Cloud comparison breaks down the specific differences for hosted deployments.
Last updated: July 2026. Streamlit's deployment landscape evolves quickly—always check the official Streamlit docs for the latest Community Cloud features.

