National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 19

部署上線

requirements、環境變數、Gunicorn、Nginx 與 HTTPS
Interaction實作導覽 30 minute(s)

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. 1

    記下用到哪些套件

    伺服器上要裝一模一樣的套件,所以要有一份清單。pip freeze 會列出目前環境裝了什麼、各是哪一版,存成 requirements.txt。

    一定要在專案自己的虛擬環境裡做,否則會把電腦上所有不相關的套件都列進去。waitress 是 Windows 也能用的正式 WSGI 伺服器。

  2. 2

    產生一把真正的 SECRET_KEY

    secret_key 用來簽 Session Cookie。寫在程式裡等於跟著程式碼一起進 git,看得到原始碼的人就能偽造任何人的登入狀態。

    正確做法是產生一串夠長的隨機值,放在環境變數。下面這個值只是輸出範例,已經公開在這一頁,不要拿來用。

  3. 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. 4

    設定好再啟動

    環境變數設定好就正常了。app:app 的意思是「app.py 這個模組裡的 app 變數」。

    再開一個終端機用 curl 試。回應標頭的 Server: waitress 表示這次不是開發伺服器在回答。PowerShell 裡的 curl 可能是別的指令,所以打 curl.exe。

  5. 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 保護,官方仍然明確要求不能用在正式環境。下面的模擬瀏覽器只顯示簡化的錯誤訊息,不是真的偵錯器,但已經足以看出問題。

互動練習:除錯模式、環境變數與 ProxyFix

按「啟動伺服器」。這裡的 .env 分頁相當於伺服器上的環境變數,改完按「存檔並重新啟動」才會生效。

下面有一個 API 工具可以自己加請求標頭,用來假裝是 Nginx 轉來的請求。第 6 個任務要加的程式:

from werkzeug.middleware.proxy_fix import ProxyFix          # 放在檔案最上面

app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)   # 放在 app = Flask(__name__) 之後

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 和自訂服務,這幾步用「(說明)」按鈕告訴你實際上要下什麼指令。

互動練習:在 Ubuntu 上接好 Nginx 與 systemd

專案已經放在 /srv/notes。依序按下面的按鈕,每一顆都讀一下輸出。其中 nginx -t 那一顆故意沒加 sudo,會失敗。資料夾樹會顯示檔案權限。

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 的層數和實際的代理層數相同
  • 資料庫有備份,而且試過還原
  • 服務當掉會自動重啟,日誌看得到

想用容器或雲端服務部署,觀念相同,只是有些工作交給平台處理:見 Docker 軌 與 雲端軌。

動手做:讀取部署設定

完成 load_settings:從環境變數(這裡傳字典,方便測試)取出設定並轉成正確的型別。

  • SECRET_KEY 必要:沒有或是空字串時 raise RuntimeError('缺少 SECRET_KEY')
  • WORKERS 轉成整數,沒有設定時是 2
  • COOKIE_SECURE 只有字串 '0' 時才是 False,其他情況(包含沒有設定)都是 True

程式最下面的測試不要改。

你的輸出
預期輸出

想一想

1.

關於 Nginx 與 Gunicorn 的分工,下面哪一項正確?

2.

前面有 Nginx 時,Gunicorn 為什麼要綁 127.0.0.1:8000,不綁 0.0.0.0:8000?

3.

下面的程式會印出什麼?

import os
os.environ['APP_DEBUG'] = '0'
print(bool(os.environ['APP_DEBUG']))
4.

不小心把含有 SECRET_KEY 的 .env 推上 GitHub,下一步最該做什麼?

5.

使用者看到 502 Bad Gateway,Nginx 的 error.log 寫著 connect() failed (111: Connection refused) while connecting to upstream。最可能的原因是?

第 1 次作答

重點整理

  • 開發伺服器不能上線;正式環境用 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、登入與權限組成一個小型課程討論區。

這一課完成了!
Next lesson:專題:小型課程討論區
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。