Skip to main content

Module 10 — Deployment and access control

Nine modules built the app. This last module puts it in front of users and keeps it from ending up in front of the wrong ones. It covers the two deployment paths every Streamlit team takes at some point — Community Cloud and a private container — and the two access-control layers those deployments need: secrets that never enter git, and a front door that only the sales team walks through.

Community Cloud: the fastest path

Streamlit Community Cloud is the free hosting tier from the Streamlit team. It reads a GitHub repository, installs the dependencies listed in requirements.txt (or pyproject.toml), and runs streamlit run app.py behind a public HTTPS URL. From a first commit to a live URL is about ninety seconds.

The workflow assumes three files at the root of the repository.

# requirements.txt — pinned versions save you the moment Streamlit ships a breaking release
streamlit==1.36.0
pandas==2.2.2
scikit-learn==1.5.0
joblib==1.4.2
plotly==5.22.0

# runtime.txt — optional, pins the Python version
python-3.11

# .streamlit/config.toml — the theme from module 9
[theme]
primaryColor = "#1F4E79"

Two habits pay off immediately. Pin every dependency with ==; Community Cloud rebuilds on every push and a new NumPy patch can silently break joblib.load(). And do not commit the model: a 200 MB .joblib file makes the repo painful to clone and is not what git is designed for. Store it in an object bucket and load it from an authenticated URL, or use the st.file_uploader pattern for a local demo.

Community Cloud puts the app on a public URL by default. For an internal tool, either restrict it with the platform's "restricted access" option (which requires visitors to sign in with a whitelisted email) or put the password gate below in front of it.

A private container with Docker

Once the app must live inside the corporate network or scale beyond Community Cloud's resource limits, a container is the standard next step. A minimal Dockerfile is fifteen lines.

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Install dependencies first so Docker caches the layer.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the app.
COPY . .

EXPOSE 8501

# --server.address 0.0.0.0 exposes the server outside the container's loopback.
ENTRYPOINT ["streamlit", "run", "app.py", \
"--server.port=8501", \
"--server.address=0.0.0.0", \
"--server.headless=true"]

# HEALTHCHECK for orchestrators: Streamlit answers /_stcore/health with 200 when healthy.
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl --fail http://localhost:8501/_stcore/health || exit 1

Two lines are the ones people forget. --server.address=0.0.0.0 is mandatory when the app runs in a container; the default localhost would only accept connections from inside the container, and the outside world would see a refused connection. --server.headless=true prevents Streamlit from trying to open a browser on the container's host, which is not what you want in production.

Building and running is the usual Docker gesture:

# terminal
docker build -t churn-dashboard:1.0.0 .
docker run --rm -p 8501:8501 \
--env USE_REMOTE_MODEL=true \
--env-file .env \
churn-dashboard:1.0.0

Behind a load balancer, cap the container at one Streamlit process per CPU and use sticky sessions on the websocket. Streamlit keeps per-session state in memory (module 6), and hopping across replicas between requests would lose that state.

Secrets: st.secrets and the .streamlit folder

Never commit an API token, a database password or an inference URL. In development, st.secrets reads from .streamlit/secrets.toml — which must be in your .gitignore.

# .streamlit/secrets.toml — LOCAL ONLY, never committed
INFERENCE_URL = "https://models.internal.example.com/predict"
INFERENCE_TOKEN = "sk-live-abc123"

[database]
host = "db.internal"
user = "reader"
password = "s3cret"

Reading is a plain dict access, with nested tables for grouping:

import streamlit as st

url = st.secrets["INFERENCE_URL"]
db_password = st.secrets["database"]["password"]

In production the file is replaced by the platform's secret manager: environment variables in a Docker deployment, the Secrets tab in Community Cloud, Kubernetes secrets mounted as files, AWS Secrets Manager. Streamlit reads st.secrets once at startup, so a rotated secret requires a restart — plan the rotation accordingly.

Password gate: the simplest access control

For an internal dashboard where a single shared password is acceptable, a five-line gate at the top of the script is enough.

import hmac
import streamlit as st

def password_ok() -> bool:
def check():
expected = st.secrets["APP_PASSWORD"]
st.session_state["auth_ok"] = hmac.compare_digest(st.session_state["pwd"], expected)
if not st.session_state["auth_ok"]:
st.session_state["pwd"] = ""

if st.session_state.get("auth_ok"):
return True

st.text_input("Password", type="password", key="pwd", on_change=check)
if st.session_state.get("pwd") and not st.session_state.get("auth_ok"):
st.error("Wrong password.")
return False

if not password_ok():
st.stop()

# ... the rest of the app ...

Two details keep this from being trivially wrong. hmac.compare_digest is a constant-time comparison, so an attacker cannot infer the password by timing repeated attempts. And the wrong password is cleared from session_state on failure, so a subsequent rerun does not keep trying it. Even with those two details, a shared password is the weakest access control there is; use it for an internal, low-value dashboard, not for anything with real customer data.

Delegated authentication for the real cases

Any team that owns real customer data will need a real identity provider: Google Workspace, Microsoft Entra ID, Okta, Auth0. Two serious paths exist for Streamlit.

Native OIDC — Streamlit ships built-in OpenID Connect since version 1.36. Configure [auth] in secrets.toml with the provider's client_id, client_secret and redirect_uri, and st.experimental_user gives you the signed-in user's identity from anywhere in the script. This is the direction the framework is investing in and the default choice going forward.

Reverse proxy — nginx, Envoy or a cloud load balancer handles the OIDC dance in front of Streamlit, forwards the user identity in an HTTP header, and the app reads it through st.context.headers["X-Forwarded-User"]. This shifts the identity provider integration out of the Python code and into the infrastructure, which is often what a security team prefers.

Whichever you pick, do the authorization check inside the app, not just at the door: an authenticated user is not necessarily a whitelisted one, and different pages of the multipage app (module 9) may need different permissions.

Concurrency limits, in one paragraph

A single Streamlit process handles multiple sessions cooperatively. Every rerun runs Python code on the main thread — a slow scoring function will block other users for its duration. If two users hit the model concurrently and the model takes eight seconds each, the second one waits sixteen. The right answers are: move the model behind a remote API (module 8) so the Streamlit process spends its time in network I/O the event loop can multiplex, and horizontally scale the Streamlit container. The wrong answer is Python threads; they will not save you against a CPU-bound model.

Deploying the running example

The final push is three files: the Dockerfile above, a requirements.txt with pinned versions, and an updated .streamlit/config.toml with maxUploadSize for the batch tab. The image is built by a CI job on every push to main, tagged with the git SHA, and deployed by the platform team behind an OIDC gate that only lets the sales operations Active Directory group in.

A secret in git stays in git forever

git rm secrets.toml && git commit does not remove the file from the history; a git log --all --full-history -- .streamlit/secrets.toml will still find it, and so will any attacker who clones the repo. Rotate the token immediately if it ever leaks, then use git filter-repo or a fresh repo to rewrite history. Prevention is .gitignore on day one and a pre-commit hook (git-secrets, detect-secrets) after that.

In summary

  • Community Cloud for a public demo or an internal tool with restricted access; Docker for anything else, with --server.address=0.0.0.0 and --server.headless=true.
  • Pin every dependency in requirements.txt; never commit the model or .streamlit/secrets.toml.
  • Secrets: st.secrets in dev, environment variables or the platform's secret manager in production; rotation requires a restart.
  • Access control: a shared password with hmac.compare_digest for low-value dashboards, native OIDC or a reverse proxy for anything else; authorize inside the app, not just at the door.

Next: the recap and the 40-question exam.