2025年7月7日 星期一

Mapleboard MP510-50 測試 (二十六) : 用獨立站台執行應用程式 (1)

在前一篇測試中, 我們使用 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) 測試 app127.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  (建立站台設定檔)
sudo ln -s /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/ (建立 Symbolic link)
6. 刪除預設站台鏈結sudo rm /etc/nginx/sites-enabled/default
7. 測試與重新載入 Nginxsudo 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 埠. 

沒有留言 :