在前一篇測試中, 我們使用 Nginx 預設的網站設定檔 default, 成功地架設了基於 Nginx + Gunicorn 的 Flask 應用伺服器. 這種方式適合快速測試或僅需部署單一小型網站的情境; 然而, 當需要部署多個網站, 或同時管理多個 Flask 或 Django 專案時, 應該改用獨立的網站設定檔搭配不同的子網域 (或單一設定檔使用不同的 location /path/ 導向), 以利彈性調整與方便維護.
本篇將示範如何建立獨立站台的 Nginx 設定檔 hello, 並搭配 Certbot 建立 HTTPS 憑證, 取代使用預設的 default 設定. 這種作法不僅較具可擴充性, 也能避免日後多站台設定互相干擾, 更方便依據不同網域或子網域調整反向代理路徑與 SSL 設定. 本篇僅測試以獨立設定檔取代預設站台設定檔 default 的方法, 並未涉及用子網域處理多網站議題.
本系列全部的測試紀錄參考 :
注意, 在前一篇測試中我們已經完成應用程式 hello.py 的本機測試, 然後在應用程式 hello.py 所在目錄啟動 Gunicorn 伺服器來監聽 127.0.0.1 的 8080 埠 (注意, 必須在 app 所在目錄啟動 Gunicorn 伺服器) :
tony1966@LX2438:~$ cd flask_apps
tony1966@LX2438:~/flask_apps$ gunicorn -w 4 -b 127.0.0.1:8080 hello:app
可以用下列指令來檢查是否有 Gunicorn 程序正在運行 :
tony1966@LX2438:~$ ps aux | grep gunicorn
tony1966 223187 0.0 0.4 34156 19296 ? Ss Jul06 1:11 gunicorn: master [hello:app]
tony1966 223192 0.0 0.6 42348 26324 ? S Jul06 0:07 gunicorn: worker [hello:app]
tony1966 223193 0.0 0.6 42344 26244 ? S Jul06 0:07 gunicorn: worker [hello:app]
tony1966 223194 0.0 0.6 42344 26304 ? S Jul06 0:07 gunicorn: worker [hello:app]
tony1966 223195 0.0 0.6 42344 26364 ? S Jul06 0:07 gunicorn: worker [hello:app]
tony1966 273405 0.0 0.0 9136 1748 pts/0 S+ 21:34 0:00 grep --color=auto gunicorn
可見有四個 worker 程序在執行 hello.py 程式.
用下列指令則可檢視是否有程序在監聽 8080 埠 :
tony1966@LX2438:~$ sudo lsof -i :8080
[sudo] tony1966 的密碼:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
gunicorn: 223187 tony1966 5u IPv4 3082443 0t0 TCP localhost:http-alt (LISTEN)
gunicorn: 223192 tony1966 5u IPv4 3082443 0t0 TCP localhost:http-alt (LISTEN)
gunicorn: 223193 tony1966 5u IPv4 3082443 0t0 TCP localhost:http-alt (LISTEN)
gunicorn: 223194 tony1966 5u IPv4 3082443 0t0 TCP localhost:http-alt (LISTEN)
gunicorn: 223195 tony1966 5u IPv4 3082443 0t0 TCP localhost:http-alt (LISTEN)
可見就是上面的 Gunicorn worker 程序 (PID 一致).
最後為了讓系統當機重啟開機後會自動啟動 Gunicorn 來執行應用程式, 或是若應用程式當掉也能自動重啟, 我們也將原本手動啟動 Gunicorn 執行 app 的動作設定為一個名為 hello 的系統服務 (systemd), 可用下列指令查看這個系統服務 hello 的狀態 :
tony1966@LX2438:~$ sudo systemctl status hello
● hello.service - Gunicorn instance to serve Flask hello app
Loaded: loaded (/etc/systemd/system/hello.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2025-07-06 14:55:45 CST; 2 days ago
Main PID: 223187 (gunicorn: maste)
Tasks: 5 (limit: 4213)
Memory: 68.5M
CPU: 1min 42.464s
CGroup: /system.slice/hello.service
├─223187 "gunicorn: master [hello:app]" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >
├─223192 "gunicorn: worker [hello:app]" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >
├─223193 "gunicorn: worker [hello:app]" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >
├─223194 "gunicorn: worker [hello:app]" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >
└─223195 "gunicorn: worker [hello:app]" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" >
Jul 06 14:55:45 LX2438 systemd[1]: Started Gunicorn instance to serve Flask hello app.
Jul 06 14:55:46 LX2438 gunicorn[223187]: [2025-07-06 14:55:46 +0800] [223187] [INFO] Starting gunicorn 23.0.0
Jul 06 14:55:46 LX2438 gunicorn[223187]: [2025-07-06 14:55:46 +0800] [223187] [INFO] Listening at: http://127.0.0.1:8080 (223187)
Jul 06 14:55:46 LX2438 gunicorn[223187]: [2025-07-06 14:55:46 +0800] [223187] [INFO] Using worker: sync
Jul 06 14:55:46 LX2438 gunicorn[223192]: [2025-07-06 14:55:46 +0800] [223192] [INFO] Booting worker with pid: 223192
Jul 06 14:55:46 LX2438 gunicorn[223193]: [2025-07-06 14:55:46 +0800] [223193] [INFO] Booting worker with pid: 223193
Jul 06 14:55:46 LX2438 gunicorn[223194]: [2025-07-06 14:55:46 +0800] [223194] [INFO] Booting worker with pid: 223194
Jul 06 14:55:46 LX2438 gunicorn[223195]: [2025-07-06 14:55:46 +0800] [223195] [INFO] Booting worker with pid: 223195
lines 1-22/22 (END)
狀態 active (running) 表示服務狀態為啟用執行中. 以下測試是在此條件下, 將原本透過 Nginx 預設站台 default 轉發 HTTPS 請求的工作改成由自行建立的站台 hello 來轉發, 所以本篇旨在說明如何建立新的站台與刪除預設站台的鏈結, 以及如何使用 Let's Encrypt 所提供的 Certbot 工具來為新的站台申請 SSL/TLS 憑證.
1. 建立站台設定檔 hello :
用 nano 在 /etc/sites-available/ 下新建一個名為 hello 的網站設定檔 :
tony1966@LX2438:~$ sudo nano /etc/nginx/sites-available/hello
[sudo] tony1966 的密碼:
輸入如下內容 :
server {
listen 80;
server_name tony1966.cc www.tony1966.cc;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
按 Ctrl+O 存檔後按 Ctrl+X 跳出 nano.
2. 建立鏈接啟用站台 hello :
其實就是將 /etc/nginx/sites-available/hello 於 /etc/nginx/sites-enabled/ 下建立鏈接 :
tony1966@LX2438:~$ sudo ln -s /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/
檢視 /etc/nginx/sites-enabled/ 下會有兩個鏈接 :
tony1966@LX2438:~$ ls -ls /etc/nginx/sites-enabled/
總用量 0
0 lrwxrwxrwx 1 root root 34 Mar 2 2023 default -> /etc/nginx/sites-available/default
0 lrwxrwxrwx 1 root root 32 Jul 7 15:23 hello -> /etc/nginx/sites-available/hello
3. 移除預設站台 default 的鏈接 :
移除 /etc/nginx/sites-enabled/default 鏈接 :
tony1966@LX2438:~$ sudo rm /etc/nginx/sites-enabled/default
[sudo] tony1966 的密碼:
注意, 並不會影響本預設站台的本尊 /etc/nginx/sites-available/default :
tony1966@LX2438:~$ ls -ls /etc/nginx/sites-available
總用量 12
8 -rw-r--r-- 1 root root 4492 Jul 5 13:11 default
4 -rw-r--r-- 1 root root 345 Jul 7 15:19 hello
可見 default 的本尊還存在. 檢查已啟用的網站設定檔 :
tony1966@LX2438:~$ ls -ls /etc/nginx/sites-enabled
總用量 0
0 lrwxrwxrwx 1 root root 32 Jul 7 15:23 hello -> /etc/nginx/sites-available/hello
可見只剩 hello 了.
4. 檢查網站設定檔與重新載入 Nginx :
用 nginx -t 指令測試 Nginx 的網站設定檔, 看看是否有語法錯誤以及配置是否正確
tony1966@LX2438:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
沒有錯誤就重新載入 Nginx :
tony1966@LX2438:~$ sudo systemctl reload nginx
[sudo] tony1966 的密碼:
tony1966@LX2438:~$
5. 使用 Certbot 自動加入 HTTPS 設定 :
這是關鍵步驟, 因為我申請的域名 tony1966.cc 有向 Let's Encrypt 註冊 SSL/TLS 憑證, 但上面的 hello 網站設定檔中並沒有憑證的資訊, 這需要用 Let's Encrypt 提供的工具程式 Certbot 來幫忙設定, 指令如下 :
sudo certbot --nginx -d tony1966.cc -d www.tony1966.cc
此指令的參數說明如下 :
- --nginx :
指定使用 Nginx 插件去自動編輯 Nginx 設定檔並重載服務 - -d tony1966.cc -d www.tony1966.cc :
指定要申請 SSL 憑證的網域名稱 (可列舉多個), 此處的對象是 tony1966.cc 和 www.tony1966.ccCertbot
此 Certbot 指令會為這兩個網域申請或更新 SSL 憑證並自動設定 Nginx 的 HTTPS 服務. Certbot 會先驗證網域擁有權, 確認這台伺服器是 tony1966.cc 的擁有者後, 會向 Let's Encrypt 請求 SSL 憑證, 如果成功就會產生兩個重要檔案 :
- /etc/letsencrypt/live/tony1966.cc/fullchain.pem (公鑰憑證)
- /etc/letsencrypt/live/tony1966.cc/privkey.pem (私鑰)
然後去掃描 /etc/nginx/sites-enabled/ 下的所有網站設定檔, 找出包含對應的 server_name 的設定檔後, 它會去編輯這些設定檔在 /etc/nginx/sites-available/ 下的本尊, 加入 listen 443 ssl 與ssl_certificate 等設定資訊, 並設定 HTTP ➜ HTTPS 的自動轉向強制轉成 HTTPS :
tony1966@LX2438:~$ sudo certbot --nginx -d tony1966.cc -d www.tony1966.cc
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
You have an existing certificate that contains a portion of the domains you
requested (ref: /etc/letsencrypt/renewal/tony1966.cc.conf)
It contains these names: tony1966.cc
You requested these names for the new certificate: tony1966.cc, www.tony1966.cc.
Do you want to expand and replace this existing certificate with the new
certificate?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(E)xpand/(C)ancel: E
Renewing an existing certificate for tony1966.cc and www.tony1966.cc
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/tony1966.cc/fullchain.pem
Key is saved at: /etc/letsencrypt/live/tony1966.cc/privkey.pem
This certificate expires on 2025-10-05.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
Deploying certificate
Successfully deployed certificate for tony1966.cc to /etc/nginx/sites-enabled/hello
Successfully deployed certificate for www.tony1966.cc to /etc/nginx/sites-enabled/hello
Your existing certificate has been successfully renewed, and the new certificate has been installed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
tony1966@LX2438:~$
注意, 執行一半會出現是否要 Expand 的詢問, 這是因為 certbot 指令中我們用兩個 -d 參數指定了兩個網域, 要回答 E (擴展), 這樣新申請的憑證會取代原本只含 tony1966.cc 的憑證, 擴展成包含 tony1966.cc 和 www.tony1966.cc 的新憑證.
Certbot 執行成功後會簽發包含 www 子網域的新憑證, 自動修改對應的 Nginx 設定檔, 並加入Certbot 自動設定續期機制 (因 Let's Encrypt 憑證效期是 90 天), 最後重啟 Nginx 伺服器.
檢視一下 Certbot 修改過的 hello 站台設定檔 :
tony1966@LX2438:~$ cat /etc/nginx/sites-available/hello
server {
server_name tony1966.cc www.tony1966.cc; # 指定這個伺服器區塊所對應的網域名稱
location / { # 當網址路徑為 / 時(也就是任何請求)
proxy_pass http://127.0.0.1:8080/; # 將請求反向代理轉發到本機 port 8080 的 Flask/Gunicorn 應用
proxy_set_header Host $host; # 保留原始的 Host 標頭
proxy_set_header X-Real-IP $remote_addr; # 傳送用戶的真實 IP 給後端
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 傳送完整的轉發鏈 IP 給後端
proxy_set_header X-Forwarded-Proto $scheme; # 傳送使用的協定(HTTP 或 HTTPS)給後端
}
listen 443 ssl; # 監聽 HTTPS 連線的 443 埠,並啟用 SSL(由 Certbot 管理)
ssl_certificate /etc/letsencrypt/live/tony1966.cc/fullchain.pem; # SSL 公鑰憑證路徑(由 Certbot 建立)
ssl_certificate_key /etc/letsencrypt/live/tony1966.cc/privkey.pem; # SSL 私鑰路徑(由 Certbot 建立)
include /etc/letsencrypt/options-ssl-nginx.conf; # SSL 安全選項(由 Certbot 提供的最佳化設定)
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # Diffie-Hellman 參數檔案(加強加密安全)
}
server {
if ($host = www.tony1966.cc) {
return 301 https://$host$request_uri; # 若用戶輸入的是 http://www.tony1966.cc,則重新導向到 https
} # managed by Certbot
if ($host = tony1966.cc) {
return 301 https://$host$request_uri; # 若用戶輸入的是 http://tony1966.cc,則重新導向到 https
} # managed by Certbot
listen 80; # 監聽 HTTP 連線的 80 埠
server_name tony1966.cc www.tony1966.cc; # 對應的網域名稱
return 404; # 若上面的 if 沒命中(應該不會發生),就回傳 404 錯誤
}
這時就可順利拜訪 https://tony1966.cc 與 https://www.tony1966.cc 這兩個網域了 (這是透過獨立的網站設定檔 hello 而非預設站台的 default). 我測試下列 URL 均正常, 結果與前一篇使用 default 站台時一樣 :
關於 Certbot 指令用法摘要說明如下表 :
| 使用情境 | Certbot 指令 | 說明 |
|---|---|---|
| 第一次為網站申請憑證 | sudo certbot --nginx -d example.com |
申請並設定 SSL,會自動修改 Nginx 設定檔 |
| 新增子網域(如 www) | sudo certbot --nginx -d example.com -d www.example.com |
若原憑證無 www,會詢問是否 expand |
| 整合多個網域至同一張憑證 | sudo certbot --nginx --cert-name example.com -d example.com -d www.example.com |
使用 --cert-name 可更新既有憑證而不新增新檔 |
| 測試是否可順利自動續期 | sudo certbot renew --dry-run |
只測試、不實際續期,檢查流程是否正常 |
| 強制立即續期 | sudo certbot renew |
會續期所有接近過期的憑證 |
| 查看目前所有憑證 | sudo certbot certificates |
查看憑證清單、網域、有效日期與檔案位置 |
| 刪除指定憑證 | sudo certbot delete --cert-name example.com |
從系統中移除該憑證與相關設定 |
如果以後要新增子網域, 例如 api.tony1966.cc, 程序如下 :
- 登入 Namecheap → Domain List → 點選網域 (tony1966.cc) → Advanced DNS
- 新增一筆記錄 :
Type : CNAME (或 A 記錄)
Host : api
Value : 填 @ (代表 tony1966.cc 的 IP) 或 IP, 因主機與主網域同一台
TTL : Automati - 新增 Nginx 網站設定 (可複製 hello)
- 建立 symlink 並測試與重新載入 Nginx
- 執行 Certbot 為子網域申請 SSL 憑證 :
sudo certbot --nginx -d api.tony1966.cc
出現 expand 選項時要選 E
6. Let's Encrypt 的 SSL/TLS 憑證自動續期 :
Let's encrypt 的 SSL/TLS 憑證有效期限為 90 天, 但 Certbot 會設定系統定時器在到期前自動更新憑證, 在安裝 Certbot 時它會自動設定一個定期執行續期任務的定時器, 可用下列指令查詢 :
tony1966@LX2438:~$ systemctl list-timers | grep certbot
Wed 2025-07-09 06:59:30 CST 15h left Tue 2025-07-08 13:15:51 CST 2h 13min ago certbot.timer certbot.service
可用 cat 檢視定時器內容 :
tony1966@LX2438:~$ sudo systemctl cat certbot.timer
[sudo] tony1966 的密碼:
# /lib/systemd/system/certbot.timer
[Unit]
Description=Run certbot twice daily
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true
[Install]
WantedBy=timers.target
檢視自動續期服務 :
tony1966@LX2438:~$ sudo systemctl cat certbot.service
# /lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://certbot.eff.org/docs
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true
當然可以手動以 sudo certbot renew 強制立即續期, 但通常只要在此指令後面加上 --dry-run 參數去模擬續期流程, 驗證自動續期是否會成功即可 (不會真的去將憑證馬上續期) :
tony1966@LX2438:~$ sudo certbot renew --dry-run
[sudo] tony1966 的密碼:
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/tony1966.cc.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Account registered.
Simulating renewal of an existing certificate for tony1966.cc and www.tony1966.cc
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/tony1966.cc/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
看到 "all simulated renewals succeeded" 表示 Certbot 成功模擬了對這兩個網域的續期流程, 證明系統上已設定好自動續期機制, 屆時 Certbot 能正確存取 Nginx 與對外服務.
7. 總結新增 Nginx 站台的流程 :
下表總結本篇與前一篇測試的程序 :
| 步驟 | 說明 |
|---|---|
| 1. 建立 Flask app | 例如 ~/flask_apps/hello.py |
| 2. 本機 (localhost) 測試 app | 127.0.0.1:port 瀏覽器測試 OK |
| 3. 手動啟動 Gunicorn 測試 | 例如~/flask_apps/gunicorn -w 4 -b 127.0.0.1:8080 hello:app |
| 4. 啟動 Gunicorn 系統服務 | sudo nano /etc/systemd/system/hello.service (編輯系統服務檔) sudo systemctl daemon-reload (重新載入 Systemd 的設定檔) sudo systemctl enable hello (啟動系統服務) sudo systemctl status hello (查看系統服務狀態) |
| 5. 新增 Nginx 站台 | sudo nano /etc/nginx/sites-available/hello (建立站台設定檔) |
| 6. 刪除預設站台鏈結 | sudo rm /etc/nginx/sites-enabled/default |
| 7. 測試與重新載入 Nginx | sudo nginx -t (測試)sudo systemctl reload nginx (重新載入) |
| 8. 用 Certbot 加入 SSL 憑證 | sudo certbot --nginx -d tony1966.cc -d www.tony1966.cc |
| 9. 驗證測試 | 瀏覽 https://tony1966.cc/ 與 https://www.tony1966.cc/ 頁面 |
注意, 為了降低安全風險與避免衝突, Gunicorn 不應監聽 0~1023 的系統服務埠 (存取這些埠需要 root 權限, 一旦攻擊者取得 root 權限後可綁定 80 或 443 來劫持流量), 應該監聽 1024~65535 的埠口, 通常使用 8080 埠 (或 8081). 透過 Nginx 的 80 與 443 port 作反向代理來接收外部請求並轉發給內部 8080 埠.
沒有留言 :
張貼留言