一. 前言:Demo 很快,上線才是修羅場 #
Streamlit 最迷人的地方,是你可以用很少的 Python 做出互動 App。 st.title()、st.file_uploader()、st.dataframe() 放一放,十分鐘就能 demo。 但 demo 跟上線是兩種生物。 Demo 的時候,你可以把 API key 寫在程式裡;上線以後,這叫事故。
Demo 的時候,你可以用本機路徑讀資料;上線以後,雲端不認識你的桌面。 Demo 的時候,你可以把檔案存在 ./uploads;上線以後,容器重啟可能全部消失。 今天拍拍君要整理 Streamlit 部署前最常撞到的工程細節:secrets、設定檔、環境變數、Cloud 部署、Docker,以及上線前 checklist。 如果你還在入門階段,可以先看 Streamlit 入門篇;如果你已經開始做多頁 App,可以接著看 Streamlit 進階篇。
這篇的目標很明確:讓你的 App 從「我電腦可以跑」變成「別人也能安心用」。
二. 建立可部署的專案骨架 #
先建立專案。 拍拍君這裡用 uv,因為 lockfile 乾淨、速度也快。
uv init streamlit-deploy-demo
cd streamlit-deploy-demo
uv add streamlit pandas requests pydantic-settings
不用 uv 也沒關係,傳統 venv 一樣可以。
python -m venv .venv
source .venv/bin/activate
pip install streamlit pandas requests pydantic-settings
pip freeze > requirements.txt
建議目錄長這樣:
streamlit-deploy-demo/
├── app.py
├── pyproject.toml
├── uv.lock
├── requirements.txt
├── .gitignore
├── .streamlit/
│ ├── config.toml
│ └── secrets.toml.example
├── src/
│ ├── settings.py
│ └── ui.py
└── README.md
注意:這裡是 secrets.toml.example,不是正式的 secrets.toml。 真正的 secret 不該進 Git。 先寫 .gitignore:
.venv/
__pycache__/
.streamlit/secrets.toml
.env
.env.*
*.sqlite
*.db
.DS_Store
這個檔案是部署安全的第一道門。 很多事故不是因為駭客太厲害,而是因為我們自己把鑰匙放在門口地毯下面。
三. 分清楚 config 與 secrets #
Streamlit 有兩種常見設定。 第一種是 Streamlit 自己的行為設定,例如 theme、server headless、port、runOnSave。 這些放在 .streamlit/config.toml,通常可以 commit。 第二種是 App 需要的秘密資訊,例如 API key、資料庫密碼、第三方服務 token。
這些放在 .streamlit/secrets.toml,或部署平台提供的 secrets 面板。 先看可 commit 的 config.toml:
[theme]
base = "light"
primaryColor = "#4f46e5"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f8fafc"
textColor = "#0f172a"
[server]
headless = true
runOnSave = false
接著建立範例 secret 檔。
# .streamlit/secrets.toml.example
APP_ENV = "local"
API_BASE_URL = "https://api.example.com"
API_KEY = "replace-me"
[database]
url = "postgresql://user:password@host:5432/app"
本機開發時複製一份:
cp .streamlit/secrets.toml.example .streamlit/secrets.toml
然後把值改成真的。 再次提醒:.streamlit/secrets.toml 必須被 .gitignore 擋住。
四. 用 st.secrets,但不要散落各頁 #
Streamlit 讀 secrets 很簡單。
import streamlit as st
api_key = st.secrets["API_KEY"]
api_base_url = st.secrets.get("API_BASE_URL", "https://api.example.com")
這樣可以跑,但 App 長大以後,你不會想在每個頁面都直接碰 st.secrets。 拍拍君建議把設定集中到 src/settings.py。
from dataclasses import dataclass
import streamlit as st
@dataclass(frozen=True)
class Settings:
app_env: str
api_base_url: str
api_key: str
database_url: str | None = None
def load_settings() -> Settings:
database = st.secrets.get("database", {})
return Settings(
app_env=st.secrets.get("APP_ENV", "local"),
api_base_url=st.secrets.get("API_BASE_URL", "https://api.example.com"),
api_key=st.secrets["API_KEY"],
database_url=database.get("url"),
)
在 app.py 使用它。
import streamlit as st
from src.settings import load_settings
settings = load_settings()
st.set_page_config(page_title="拍拍君部署 Demo", page_icon="🚀")
st.title("🚀 拍拍君部署 Demo")
st.caption(f"目前環境:{settings.app_env}")
if settings.api_key:
st.success("Secrets 載入成功。")
集中設定的好處是:本機、staging、production 都可以用同一份程式碼,只靠設定切換。
五. 不要把 secret 印出來 #
這句聽起來像廢話,但真的很常發生。 Debug 時很多人會寫:
st.write(st.secrets)
print(settings.api_key)
本機看起來沒事,上線以後 log 可能被平台保存、被團隊成員看到,甚至進入外部 log 系統。 比較安全的方式,是只確認 secret 是否存在。
if settings.api_key:
st.sidebar.success("API key loaded")
else:
st.sidebar.error("API key missing")
如果真的需要顯示,也只顯示遮罩版。
def mask_secret(value: str, keep: int = 4) -> str:
if not value:
return "<missing>"
if len(value) <= keep:
return "*" * len(value)
return "*" * (len(value) - keep) + value[-keep:]
正式 UI 通常連 masked secret 都不用放。 拍拍君的原則很簡單:secret 只拿來用,不拿來展示。
六. 設定分層:local、staging、production #
小 App 可以只有一份設定。 但只要你有測試環境與正式環境,就會需要分層。 最簡單的做法,是用 APP_ENV 決定要讀哪組預設值。
DEFAULT_API_URLS = {
"local": "http://localhost:8000",
"staging": "https://staging-api.example.com",
"production": "https://api.example.com",
}
def get_api_base_url(app_env: str) -> str:
return st.secrets.get("API_BASE_URL", DEFAULT_API_URLS[app_env])
本機 .streamlit/secrets.toml 可以長這樣:
APP_ENV = "local"
API_BASE_URL = "http://localhost:8000"
API_KEY = "dev-token"
正式環境則在雲端面板放:
APP_ENV = "production"
API_BASE_URL = "https://api.example.com"
API_KEY = "real-production-token"
同一份程式碼,靠設定切環境。 這才是舒服的部署節奏。
七. Streamlit Community Cloud 部署流程 #
Streamlit Community Cloud 是最容易上手的部署方式。 適合公開 demo、小型 dashboard、教學範例、內部低流量工具。 基本流程是:
- 把專案推到 GitHub
- 到 Streamlit Community Cloud 新增 App
- 選 repo、branch、main file path
- 在 Advanced settings 裡貼 secrets
- Deploy 如果你的入口檔在根目錄,Main file path 通常填:
app.py
Cloud 會依照 requirements.txt、pyproject.toml 或環境設定安裝套件。 如果你用 uv,目前最保險的做法仍是輸出 requirements.txt 給平台。
uv export --format requirements-txt --output-file requirements.txt
正式專案建議鎖版本。
streamlit==1.45.0
pandas==2.2.3
requests==2.32.3
pydantic-settings==2.9.1
版本鎖住,今天能跑的環境,明天比較不會突然變神秘怪物。
八. 在 Community Cloud 設定 secrets #
Community Cloud 的 secrets 格式跟 .streamlit/secrets.toml 很像。 你可以直接貼 TOML。
APP_ENV = "production"
API_BASE_URL = "https://api.example.com"
API_KEY = "sk_live_xxx"
[database]
url = "postgresql://user:password@host:5432/app"
部署後,在程式裡一樣用:
st.secrets["API_KEY"]
st.secrets["database"]["url"]
如果部署後出現 KeyError,通常代表三種狀況:secret 名稱拼錯、secrets 還沒儲存成功,或 App 沒有重新啟動。 可以先寫一個安全檢查,不印出值,只列出缺少哪些 key。
REQUIRED_SECRETS = ["API_KEY", "API_BASE_URL"]
missing = [key for key in REQUIRED_SECRETS if key not in st.secrets]
if missing:
st.error(f"缺少設定:{', '.join(missing)}")
st.stop()
st.stop() 很好用。 必要設定缺失時,與其讓後面噴一堆不知所云的 exception,不如在一開始就乾淨停下來。
九. 本機開發也想用環境變數怎麼辦? #
有些團隊習慣用 .env。 Streamlit 原生偏向 st.secrets,但你也可以包一層,讓本機與部署平台共存。
uv add python-dotenv
建立 .env,記得不要 commit。
APP_ENV=local
API_BASE_URL=http://localhost:8000
API_KEY=dev-token
設定讀取可以這樣寫。
import os
import streamlit as st
from dotenv import load_dotenv
load_dotenv()
def get_secret(name: str, default: str | None = None) -> str | None:
if name in st.secrets:
return st.secrets[name]
return os.getenv(name, default)
這裡的優先順序是 st.secrets、環境變數、預設值。 部署平台負責正式 secrets,本機可以用 .env 開發。 不要把兩套邏輯散落在各頁面;集中在 settings.py,世界會和平很多。
十. 檔案上傳與暫存資料的陷阱 #
Streamlit 常用 st.file_uploader()。 本機 demo 時,你可能把上傳檔存到 ./uploads。 但很多雲端部署環境的檔案系統不是永久的。 App 重啟、重新部署、容器換機器,檔案就不見了。
所以要分清楚:
- 小型暫存:可以用記憶體或暫存目錄
- 使用者資料:要放資料庫、S3、GCS、R2 等外部儲存
- 可重建快取:可以用
st.cache_data例如只做本次分析:
import pandas as pd
import streamlit as st
uploaded = st.file_uploader("上傳 CSV", type=["csv"])
if uploaded:
df = pd.read_csv(uploaded)
st.dataframe(df.head())
這樣不需要寫硬碟。 如果你真的要寫暫存檔,至少用 tempfile。
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / uploaded.name
path.write_bytes(uploaded.getbuffer())
不要假設 ./uploads 會永遠存在。 這是很多 Streamlit App 上線後第一次撞牆的地方。
十一. Docker 部署版本 #
如果你要部署到自己的 VM、Cloud Run、Fly.io、Railway 或 Kubernetes,Docker 會比較可重複。 建立 Dockerfile。
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
STREAMLIT_SERVER_HEADLESS=true \
STREAMLIT_SERVER_PORT=8501 \
STREAMLIT_SERVER_ADDRESS=0.0.0.0
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py"]
Build 與 Run:
docker build -t streamlit-deploy-demo .
docker run --rm -p 8501:8501 \
-e API_KEY="dev-token" \
-e API_BASE_URL="http://host.docker.internal:8000" \
streamlit-deploy-demo
容器裡最推薦用環境變數或平台 secret manager,不要把正式 secret bake 進 image。 錯誤示範:
ENV API_KEY=sk_live_xxx
這會讓 secret 進入 image layer。 鑰匙不應該被烤進蛋糕裡。
十二. requirements、pyproject 與版本鎖定 #
部署平台最常見的另一種爆炸,是套件版本漂移。 今天 pip install streamlit pandas 可以跑。 一個月後,某個套件升版,API 行為變了,App 突然壞掉。 最簡單的防線是 requirements.txt 鎖版本。
pip freeze > requirements.txt
如果你用 uv,可以保留 uv.lock,並在需要給平台時輸出 requirements。
uv export --format requirements-txt --output-file requirements.txt
實務上拍拍君會這樣做:repo 裡保留 pyproject.toml、保留 lockfile、部署平台需要時產生 requirements.txt,重要 App 鎖 major/minor 版本。 例如:
streamlit>=1.44,<2
pandas>=2.2,<3
requests>=2.32,<3
這樣既不會太死,也不會完全裸奔。
十三. 上線前安全 checklist #
部署前,拍拍君建議逐項檢查:
-
.streamlit/secrets.toml沒有被 Git 追蹤 -
.env沒有被 Git 追蹤 - GitHub repo 沒有歷史 secret
-
requirements.txt或 lockfile 已更新 - App 啟動時會檢查必要 secrets
- UI 不會印出完整 API key
- log 不會印出完整 token
- production API URL 不是 localhost
- 使用者上傳資料沒有依賴永久本機檔案
- README 寫清楚部署方式與必要設定 可以用這個指令確認 secret 檔沒有被追蹤。
git status --short .streamlit/secrets.toml .env
如果你不小心 commit 過 secret,光是刪掉新 commit 不一定夠。 要立刻 revoke / rotate 那個 key,清理 Git history,檢查部署平台與 log。 先換鑰匙,再掃地。 順序不要反。
十四. 一個迷你完整範例 #
最後放一個乾淨版本。 src/settings.py:
import os
from dataclasses import dataclass
import streamlit as st
@dataclass(frozen=True)
class Settings:
app_env: str
api_base_url: str
api_key: str
def get_setting(name: str, default: str | None = None) -> str | None:
if name in st.secrets:
return st.secrets[name]
return os.getenv(name, default)
def load_settings() -> Settings:
api_key = get_setting("API_KEY")
if not api_key:
raise RuntimeError("API_KEY is required")
return Settings(
app_env=get_setting("APP_ENV", "local"),
api_base_url=get_setting("API_BASE_URL", "http://localhost:8000"),
api_key=api_key,
)
app.py:
import streamlit as st
from src.settings import load_settings
st.set_page_config(page_title="拍拍君部署 Demo", page_icon="🚀")
st.title("🚀 Streamlit Deploy Demo")
try:
settings = load_settings()
except RuntimeError as exc:
st.error(str(exc))
st.stop()
st.caption(f"Environment: {settings.app_env}")
st.write("如果你看到這行,代表設定已經成功載入。")
這個範例小歸小,但已經包含幾個重要習慣:設定集中管理、缺少 secret 時提早停止、不把 secret 印出來、本機與部署環境可以用不同來源。 很多穩定的內部工具,就是從這種小骨架長大的。
結語:部署不是最後一步,是設計的一部分 #
Streamlit 讓我們很容易做出「可以互動」的 App。 但如果你希望 App 真的被別人使用,部署就不能只是最後丟上去的動作。 你要一開始就想:設定放哪裡?secret 怎麼管理?套件版本怎麼鎖?資料存在哪裡?壞掉時怎麼知道? 這些問題不華麗,但很重要。 拍拍君的建議是:就算只是小工具,也先養成三個習慣。 第一,所有 secret 都離開程式碼。 第二,所有環境差異都集中在 settings。 第三,部署方式寫進 README。 做到這三件事,你的 Streamlit App 就會從「我電腦可以跑」升級成「大家都可以安心用」。 這才是真正有用的小工具。🚀