Learning Objectives
- 說明開發伺服器為什麼不能上線,以及 Nginx、Gunicorn、Flask 各自負責什麼
- 用 requirements.txt 與環境變數,把套件清單和機密設定從程式裡分開
- 寫出反向代理的 Nginx 設定,並用 ProxyFix 讓 Flask 看到真實的來源
- 用 systemd 或 supervisor 讓網站持續執行,並知道出問題時要看哪一份日誌
- 依檢查清單確認一個網站能不能對外開放
在自己電腦上能跑,還差什麼
前面的課都用 flask run 或 app.run() 啟動網站。這個伺服器是給開發用的,Flask 啟動時也會自己提醒你:
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
Flask 文件說得很直接:開發伺服器只用於本機開發,設計上沒有特別考慮安全、穩定與效率。而且「正式環境」不是看使用者多寡,只要不是在開發,就算只有一個人用也算。
網站上線通常分成三層,各做一件事:
瀏覽器
│ HTTPS(443 埠)
Nginx
│ HTTP(127.0.0.1:8000)
Gunicorn
│ WSGI
Flask(你的程式)
- Nginx(反向代理):接外面的連線、處理 HTTPS、直接回傳靜態檔,其他請求轉給 Gunicorn。它能應付速度慢的連線;而且 80、443 埠要有權限才能開,不該讓你的程式用 root 執行。
- Gunicorn(WSGI 伺服器):開好幾個 worker 行程,把 HTTP 請求轉成 Flask 看得懂的呼叫。開發伺服器不是為正式環境設計的,這一層才是。
- Flask:你的程式,只管路由、邏輯與資料,不用處理前面兩層的事。
這一課照順序走一遍:整理專案、把機密移出程式、接上 Gunicorn 與 Nginx、最後是 HTTPS 與上線前檢查。瀏覽器跑不了這些伺服器,所以會用三種方式練習:實際輸出的操作步驟、模擬的 Ubuntu 終端機、模擬瀏覽器裡真的 Flask。
操作:把專案整理成可以搬走的樣子
下面的 Windows 指令與輸出是實際執行的結果(Windows 11、Python 3.12)。版本號會隨時間變動,以你自己跑出來的為準。macOS、Linux 把 .venv\Scripts\activate 換成 source .venv/bin/activate,設定環境變數換成 export SECRET_KEY=…,type 換成 cat。
-
1
記下用到哪些套件
伺服器上要裝一模一樣的套件,所以要有一份清單。
pip freeze會列出目前環境裝了什麼、各是哪一版,存成requirements.txt。一定要在專案自己的虛擬環境裡做,否則會把電腦上所有不相關的套件都列進去。
waitress是 Windows 也能用的正式 WSGI 伺服器。 -
2
產生一把真正的 SECRET_KEY
secret_key用來簽 Session Cookie。寫在程式裡等於跟著程式碼一起進 git,看得到原始碼的人就能偽造任何人的登入狀態。正確做法是產生一串夠長的隨機值,放在環境變數。下面這個值只是輸出範例,已經公開在這一頁,不要拿來用。
-
3
少了設定,啟動時就失敗
app.py用os.environ['SECRET_KEY']讀金鑰,並且不給預設值。忘記設定時,程式在啟動的當下就出錯,不會帶著一把大家都知道的預設金鑰上線。下面只留最後幾行輸出,檔案路徑也縮短了。import os from flask import Flask, request app = Flask(__name__) app.secret_key = os.environ['SECRET_KEY'] @app.route('/') def index(): return f'哈囉,來源 {request.remote_addr}' -
4
設定好再啟動
環境變數設定好就正常了。
app:app的意思是「app.py這個模組裡的app變數」。再開一個終端機用
curl試。回應標頭的Server: waitress表示這次不是開發伺服器在回答。PowerShell 裡的curl可能是別的指令,所以打curl.exe。 -
5
在 Linux 伺服器上換成 Gunicorn(輸出為示意)
多數正式環境是 Linux,常用 Gunicorn。Gunicorn 不支援 Windows(只能在 WSL 裡跑),所以這一步我們沒有實際執行,輸出是依官方文件整理的示意,實際上每行前面還會有時間,版本與行程編號也不同。
-w 3:開 3 個 worker。Flask 文件建議從「CPU 核心數 × 2」開始試,再依實際負載調整-b 127.0.0.1:8000:只接受本機的連線。前面有 Nginx 時不要用0.0.0.0,否則別人可以繞過 Nginx 直接連- 不要用 root 執行,否則你的程式也會有 root 權限
逐步看:設定怎麼從環境變數進到 Flask
注意兩件事:必要設定和選用設定的寫法不同;環境變數讀進來一律是字串。
.env 與 .gitignore
開發時每次開終端機都要設定環境變數很麻煩,常見作法是寫在專案裡的 .env 檔,再用 python-dotenv 這類套件載入。兩個檔案要分清楚:
| 檔案 | 內容 | 進不進 git |
|---|---|---|
.env |
真正的金鑰、資料庫密碼 | 不進,寫進 .gitignore,每台機器各有一份 |
.env.example |
只有變數名稱,值留空 | 進,讓別人知道要設定哪些變數 |
金鑰一旦進過 git,刪掉檔案也還留在歷史紀錄裡,所以要當作已經外洩,換一把新的。換掉 SECRET_KEY 會讓所有人的 Session 失效(第 14 課),也就是全部的人都要重新登入,這是預期的結果。
放在 Nginx 後面,Flask 會看錯來源
Nginx 把請求轉給 Gunicorn 之後,對 Flask 來說,每個請求都是本機的 Nginx 用 HTTP 送來的。於是:
request.remote_addr永遠是127.0.0.1,日誌和限流都認不出真正的使用者request.scheme是http,url_for(..., _external=True)產生的網址就錯了
解法是兩邊配合:Nginx 把真實資訊放進 X-Forwarded-* 標頭,Flask 用 Werkzeug 的 ProxyFix 讀回來。
逐步看:ProxyFix 前後的差別
同樣的請求送兩次,比較 Flask 看到的來源、協定與網域。最後一個請求模擬有人偽造標頭。這裡用 test_client 當作 Nginx 轉來的請求。
只有真的在代理後面才用 ProxyFix
Flask 文件特別提醒:
- 前面沒有代理就不要加。沒有 Nginx 時,使用者可以直接把
X-Forwarded-For設成任何值,你的日誌與限流就會被騙。 - 層數要設對。前面有兩層代理(例如雲端負載平衡器加上 Nginx)就要
x_for=2,設錯一樣會信任到使用者填的值。
下面的練習把前面三件事實際操作一次:除錯模式會洩漏什麼、少了金鑰會怎樣、ProxyFix 的效果。
關於除錯模式:真實的 flask run --debug 在出錯時會顯示互動偵錯器,可以在瀏覽器裡執行任意 Python 程式。雖然有 PIN 保護,官方仍然明確要求不能用在正式環境。下面的模擬瀏覽器只顯示簡化的錯誤訊息,不是真的偵錯器,但已經足以看出問題。
Nginx 設定與讓網站持續執行
Nginx 的設定檔長這樣,改自 Flask 文件的範例。在 Ubuntu 上放在 /etc/nginx/sites-available/,再連結到 sites-enabled/ 才會生效:
server {
listen 80; # 接 80 埠
server_name notes.example.edu; # 這個設定是給哪個網域用的
location /static/ { # 靜態檔由 Nginx 直接回傳,不必經過 Python
alias /srv/notes/static/;
}
location / { # 其他請求轉給 Gunicorn
proxy_pass http://127.0.0.1:8000/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
讓網站持續執行。 直接在終端機跑 Gunicorn,一登出就停了,主機重開也不會自己回來。Linux 上交給 systemd:寫一個服務設定檔,它負責開機啟動、當掉重啟、收集輸出。Supervisor 是另一個常見的行程管理工具,觀念相同。
[Service]
Type=notify
User=student # 不用 root
WorkingDirectory=/srv/notes
EnvironmentFile=/srv/notes/.env # 環境變數從這裡來
ExecStart=/srv/notes/.venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 --access-logfile=- app:app
Restart=on-failure # 當掉就重新啟動
日誌看哪裡。 出問題時依序看這幾份:
| 位置 | 內容 |
|---|---|
/var/log/nginx/access.log |
每個請求的來源、網址、狀態碼 |
/var/log/nginx/error.log |
Nginx 自己的錯誤,例如連不到 Gunicorn |
journalctl -u notes |
Gunicorn 與你的程式輸出,Python 的 traceback 在這裡 |
使用者看到 502 Bad Gateway 時,通常是 Nginx 活著、但後面的 Gunicorn 沒有在跑或連不上,先看 error.log,再看 journalctl。
下面的模擬終端機是 Ubuntu,你要把專案接上 Nginx。這是模擬,不是真的 Linux:nginx -t 不會真的檢查語法,日誌內容也是示意。它不能執行 Python、Gunicorn 和自訂服務,這幾步用「(說明)」按鈕告訴你實際上要下什麼指令。
HTTPS、資料庫與上線前檢查
HTTPS。 沒有 HTTPS 時,密碼和 Session Cookie 都以明文經過網路。憑證可以向 Let's Encrypt 申請,它會驗證你確實控制這個網域,所以要先有網域名稱,並把 DNS 指到伺服器。Certbot 可以一次申請憑證並改好 Nginx 設定(下面只列官方指令,我們沒有實際執行):
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --nginx # 申請憑證並修改 Nginx 設定
sudo certbot renew --dry-run # 測試自動續期
有了 HTTPS,Flask 這邊要把 Session Cookie 的屬性補上,否則 Cookie 還是可能經由 HTTP 送出(屬性的意思見第 14 課):
app.config.update(SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Lax')
SQLite 夠不夠用。 SQLite 官方說它適合多數中低流量的網站。要注意的限制是同一個資料庫檔案一次只能有一個寫入者。Gunicorn 的多個 worker 寫入時會排隊,寫入很頻繁、或要分散到多台伺服器時,就該換成 MySQL 這類伺服器型資料庫(SQL 軌)。不論用哪一種資料庫,都要定期備份。
上線前檢查:
DEBUG關閉,不是用flask run或app.run()啟動SECRET_KEY是隨機值,從環境變數讀取,沒有進 git.env、資料庫檔案不在 git 裡,也不在static/底下- Gunicorn 只聽
127.0.0.1,不是用 root 執行 - 防火牆只開需要的埠(通常是 80、443 與 SSH)
- HTTPS 正常,Session Cookie 有
Secure ProxyFix的層數和實際的代理層數相同- 資料庫有備份,而且試過還原
- 服務當掉會自動重啟,日誌看得到
動手做:讀取部署設定
完成 load_settings:從環境變數(這裡傳字典,方便測試)取出設定並轉成正確的型別。
SECRET_KEY必要:沒有或是空字串時raise RuntimeError('缺少 SECRET_KEY')WORKERS轉成整數,沒有設定時是2COOKIE_SECURE只有字串'0'時才是False,其他情況(包含沒有設定)都是True
程式最下面的測試不要改。
想一想
關於 Nginx 與 Gunicorn 的分工,下面哪一項正確?
前面有 Nginx 時,Gunicorn 為什麼要綁 127.0.0.1:8000,不綁 0.0.0.0:8000?
下面的程式會印出什麼?
import os
os.environ['APP_DEBUG'] = '0'
print(bool(os.environ['APP_DEBUG']))
不小心把含有 SECRET_KEY 的 .env 推上 GitHub,下一步最該做什麼?
使用者看到 502 Bad Gateway,Nginx 的 error.log 寫著 connect() failed (111: Connection refused) while connecting to upstream。最可能的原因是?
重點整理
- 開發伺服器不能上線;正式環境用 Gunicorn(Linux)或 Waitress(Windows 也可以),前面再放 Nginx
requirements.txt記套件版本;機密放環境變數,.env不進 git- 環境變數都是字串;必要設定用
os.environ['X'],讓它在啟動時就失敗 - Nginx 用
proxy_pass轉給只聽127.0.0.1的 Gunicorn,傳X-Forwarded-*,Flask 用層數正確的ProxyFix讀回來 - 用 systemd 或 supervisor 讓網站持續執行;502 先看 Nginx 的
error.log,程式錯誤看journalctl - HTTPS 用 Let's Encrypt 與 Certbot,並打開 Cookie 的
Secure
下一課是專題:把路由、模板、資料庫、API、登入與權限組成一個小型課程討論區。