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.
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.0and--server.headless=true. - Pin every dependency in
requirements.txt; never commit the model or.streamlit/secrets.toml. - Secrets:
st.secretsin dev, environment variables or the platform's secret manager in production; rotation requires a restart. - Access control: a shared password with
hmac.compare_digestfor 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.