Django + React 電商專案練習 [18] - 透過 Nginx 部署 Django 至私有主機(No PaaS)

讓 Nginx 透過 uWSGI 代理 Django,也不用受第三方平台的任何流量及檔案限制

Posted by Young on 2022-12-08
Estimated Reading Time 13 Minutes
Words 2.9k In Total

Tech Stack

  • Linux(Ubuntu 20.04+) 主機:本專案不使用雲端 PaaS 服務(Heroku、Azure、GCP),而是自行架設 Linux 主機進行多個專案的維護
  • systemd:管理多個系統服務
  • SSH:遠端連線管理,如 OpenSSH)
  • Nginx:Web Server,反向代理
  • uWSGI:Django WSGI 伺服器,Django 與 Web Service 之間的接口

網路上部署教學非常多,但 95%都是教怎麼透過第三方部署平台例如 Heroku,Azure,GCP…等,但我就是想架在自己架設的 Ubuntu 主機上阿!而關於各式部署方法及各自的優缺點我會再另外寫一篇文章,此篇就不多加贅述

DNS 基礎知識

當使用者輸入 Domain Name 後,Web 必須要先去一台有 Domain Name 和 IP 對應資料的主機去查詢這台電腦的 IP,而這台被查詣的主機為 Domain Name Server,簡稱 DNS.

Ex:當你輸入www.shopee.com.tw 時,Web 就會將 www.shopee.com.tw傳送最近的 DNS Server 去做辨識,若查詢有結果,則會傳回這台主機的 IP,進而跟它索取資料,若沒查到,就會發生 DNS NOT FOUND 的情形,而每個 Domain Name 對應要一組 IP,且 Domain Name 跟 IP 一樣不會重覆

一般架設網站時不管是 Code from Scratch 還是用 Wordpress 等都需申請 DNS 指向你的 IP,畢竟大家也不可能去記 IP 位址來找網站,申請完後才辦法讓自己以外的人找到你自行定義的網域

購買網域

我購買網域的平台為 cloudflare,個人認為他的平台是我用過 UI 最人性化且支援暗黑模式的,未來有購買網域需求不妨去 Cloudflare 網域管理平台 看看

Cloudflare 設定

在 akebee 主網域下新增一個子網域 niceshop.akebee.com,並設定 SSL/TLS 加密

cloudflare_setting

測試是否網域已生效

可透過 nslookup 來查看 DNS 設定是否生效

1
nslookup niceshop.akebee.com

若成功就會顯示:

1
2
3
4
5
6
Server:		1.1.1.1
Address: 1.1.1.1#53

Non-authoritative answer:
Name: niceshop.akebee.com
Address: 61.xx.xx.xx

若網路環境或主機設定有防火牆或其他限制,可能會導致從遠端無法連接到你的主機,也就是說用 0.0.0.0:8000 去 runserver 反而會連不到。我的情況就是只有內部網路能存取

1
2
3
4
#settings.py
DEBUG = True

ALLOWED_HOSTS = ["niceshop.akebee.com", "127.0.0.1", "localhost"]

ALLOWED_HOSTS 預設為空白,意思就是除了 localhost 以外其他的外部 IP 都無法連上這個網站

資料庫設定

sqlite3 是 Django 預設的資料庫,開發階段還能用,但在正式環境中,會採用 MySQL 或 PostgreSQL 作為正式的資料庫較合適

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# settings.py
# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
# }

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'maShop',
'USER':'www',
'PASSWORD':'Buty_2350973',
'HOST':'192.168.1.101',
'PORT':'3306',
'OPTIONS':{
'init_command':"SET sql_mode='STRICT_TRANS_TABLES'"
}
}
}

建立 Virtual Enviornment(虛擬環境)

連線至自己的主機,安裝並建立一個獨立的虛擬環境,避免不同專案之間的套件衝突

1
2
3
sudo apt-get install python3-venv
python3 -m venv django_venv
source django_venv/bin/activate

uWSGI

作為 應用伺服器(WSGI Server),負責處理來自 Nginx 的請求,並透過 WSGI 協定與 Django 互動,執行 Python 程式並回應結果。

uWSGI 與 Gunicorn 比較

比較項目 uWSGI Gunicorn
開發語言 C Python
設定難度 較複雜,支援 .ini 配置文件 簡單,CLI 參數即可
效能 效能高,可微調許多參數 效能可,適合中小型應用
擴展性 非常強,支援 WSGI、FastCGI、HTTP、SMTP 等多種協議 專注於 WSGI,簡單易用
內建功能 支援 Emperor 模式、記憶體快取、自動重啟等 內建功能較少,依賴第三方工具
應用場景 適合大規模、高度可調整的專案 適合大多數 Web 應用,特別是 REST API 服務
穩定性 高度可調整,但需要正確配置 默認設定已經適用大多數場景

uWSGI 配置

先在虛擬環境安裝 uWSGI

1
2
#
pip install uwsgi

切記註解「不要」寫在行後面,否則會解析到註解的內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[uwsgi]
chdir = /home/httpd/niceshop
# 確保這裡指向 Django 的 wsgi.py
module = backend.wsgi:application
home = /home/httpd/django_venv
virtualenv = /home/httpd/django_venv
pythonpath = /home/httpd/django_venv/lib/python3.12/site-packages

master = true
processes = 5
socket = /home/httpd/niceshop/niceshop.sock
chmod-socket = 666
vacuum = true
die-on-term = true
enable-threads = true

uwsgi_params

此檔案是在專案目錄底下/home/httpd/niceshop 建立 uwsgi_params

  • 用於定義轉發到 uWSGI 服務器的 HTTP 請求參數。
  • 此文件列出了所有必要參數,以便 Nginx 可以通過正確的方式將請求傳遞給 uWSGI。
  • 例如:QUERY_STRING 定義了 HTTP 請求中的查詢字符串。uWSGI 需要這些變量來識別請求的內容,並正確地將其轉發到相應的應用程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /home/httpd/niceshop/uwsgi_params
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

常用 uWSGI 指令

指令 功能
sudo systemctl start uwsgi 啟動 uWSGI
sudo systemctl restart uwsgi 重新啟動 uWSGI
sudo systemctl stop uwsgi 停止 uWSGI
sudo systemctl status uwsgi 查看 uWSGI 運行狀態
sudo systemctl enable uwsgi 設定開機自動啟動
sudo systemctl disable uwsgi 取消開機自動啟動
sudo journalctl -u uwsgi --no-pager --lines=50 查看 uWSGI 錯誤日誌
sudo journalctl -u uwsgi -f 監聽 uWSGI 日誌
sudo tail -f /var/log/nginx/niceshop.error.log 查看 Nginx 錯誤日誌

Nginx 設定檔

反向代理伺服器,負責處理靜態文件(如 HTML、CSS、JS)並將動態請求(如 API 請求)轉發至應用伺服器

這邊的做法是先忽略 NginxuWSGI 的設定,直接透過 python manage.py runserver 來啟動 Django 伺服器,然後透過 Nginx 來代理 Django 伺服器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# /etc/nginx/sites-available/niceshop
server {
listen 443 ssl;
server_name niceshop.akebee.com;

root /home/httpd/niceshop/frontend/build;
index index.html;

location / {
# 允許特定 IP
allow 192.168.1.0/24; # 表示 192.168.1.0 到 192.168.1.255
...
# 拒絕其他所有 IP
deny all;
try_files $uri /index.html;
}

location /static/ {
alias /home/httpd/niceshop/frontend/build/static/;
}

location /media/ {
# alias /home/httpd/niceshop/backend/media/;
alias /home/httpd/niceshop/backend/media/;
}

# uWSGI 代理(讓 /api/ 請求 Django 後端)
location /api/ {
proxy_pass http://127.0.0.1:8000;
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;

}
# SSL 設置
include /etc/nginx/snippets/ssl-params-yang.conf;

# 日誌
access_log /var/log/nginx/niceshop.access.log;
error_log /var/log/nginx/niceshop.error.log;

}

server {
if ($host = niceshop.akebee.com) {
return 301 https://$host$request_uri;
} # managed by Certbot


listen 80;
listen [::]:80;
server_name niceshop.akebee.com;
return 404; # managed by Certbot
}

測試環境 vs 正式環境差異

主要就差在 nginx 設定檔中的 proxy_pass 設定不同

測試環境:

1
2
3
4
5
6
7
location /api/ {
proxy_pass http://127.0.0.1:8000;
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;
}

正式環境:

1
2
3
4
5
6
7
8
9
10
11
location /api/ {
include /home/httpd/niceshop/uwsgi_params;
uwsgi_pass unix:/home/httpd/niceshop/niceshop.sock;

uwsgi_read_timeout 30;
uwsgi_send_timeout 30;
uwsgi_connect_timeout 30;

uwsgi_param SCRIPT_NAME "";
uwsgi_modifier1 30;
}

新增 Nginx 代理 /django-admin/ 到 Django

前面忘記新增讓 Nginx 代理 Django Admin 的設定,導致想訪問 Django 預設 Admin 頁面時一直導回首頁,所以新增以下設定

1
2
3
4
5
6
7
8
9
10
11
# 新增這一段 (代理 Django Admin)
location /django-admin/ {
include /home/httpd/niceshop/uwsgi_params;
uwsgi_pass unix:/home/httpd/niceshop/niceshop.sock;

uwsgi_read_timeout 30;
uwsgi_send_timeout 30;

uwsgi_param SCRIPT_NAME "";
uwsgi_modifier1 30;
}

還是要有這個能更好的管理後台

django_default_admin

測試 uWSGI 是否能讓網站正常運行

至專案目錄底下執行

1
2
cd /home/httpd/niceshop
uwsgi --ini uwsgi.ini

可以跑就可以改成用 systemd 來管理 uWSGI 服務,而不是每次都透過手動啟動

使用 systemd 讓 uWSGI 開機自動啟動

建立一個 systemd 服務,這樣即使伺服器重啟,uWSGI 也能自動啟動並保持運行。

1
sudo vim /etc/systemd/system/uwsgi.service

編輯 uwsgi.service 檔案,輸入以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Unit]
Description=uWSGI Emperor service
After=network.target

[Service]
ExecStart=/home/httpd/django_venv/bin/uwsgi --ini /home/httpd/niceshop/uwsgi.ini
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all
User=www-data
Group=www-data
RuntimeDirectory=uwsgi
RuntimeDirectoryMode=755

[Install]
WantedBy=multi-user.target

📌 確保:

  • ExecStart 指向 虛擬環境內的 uWSGI(/home/httpd/django_venv/bin/uwsgi)
  • User=www-data & Group=www-data,避免用 root 執行

最後 啟用並啟動 uWSGI 服務:

1
2
3
4
5
6
7
8
9
10
11
# 重新載入 systemd
sudo systemctl daemon-reload

# 啟動 uWSGI 服務
sudo systemctl start uwsgi

# 設定開機自動啟動
sudo systemctl enable uwsgi

# 確認 uWSGI 是否正在運行
sudo systemctl status uwsgi

大功告成

Django、Cloudflare、Nginx、uWSGI 的設定都已經完成,現在就可以透過外部去瀏覽網站了!

電商網站

常見問題

啟動 uwsgi 報錯:ImportError: No module named ‘pymysql‘ 解決方法:

uwsgi 用到系統的 python,而不是虛擬環境的 python,所以需要在虛擬環境中安裝 pymysql

1
2
3
4
source /home/httpd/django_venv/bin/activate
which python
which uwsgi
python -c "import pymysql; print('pymysql 載入成功')"

若回傳如下,代表 uwsgi 用到的 python 是系統的 python,而不是虛擬環境的 python

1
2
/home/httpd/django_venv/bin/python
/usr/local/bin/uwsgi

no module named ‘pymysql’

1
python3 -m pip install pymysql --upgrade

bind(): Permission denied [core/socket.c line 230]

這問題時常出現在啟動 uWSGI sudo systemctl start uwsgi時,通常是因為 uWSGI 沒有權限在指定的目錄下創建 socket 文件,解決方法是修改 socket 文件的權限:

1
2
sudo chown -R www-data:www-data /home/httpd/niceshop
sudo chmod -R 755 /home/httpd/niceshop

uwsgi.service 啟動失敗,錯誤碼 203/EXEC

大多是因為先前不小心直接用 root 身份去執行 uwsgi,導致權限被改變,uwsgi.service 是用 www-data 用戶和組,所以要改回來

1
2
3
4
5
6
7
8
9
10
11
12
# 重新啟用 uwsgi
sudo systemctl restart uwsgi

# 檢查 uwsig 錯誤日誌
sudo journalctl -u uwsgi --no-pager --lines=50

# 若啟用虛擬環境還是顯示 /usr/local/bin/uwsgi,代表沒啟用成功,可能就是 venv 權限的問題
source /home/httpd/django_venv/bin/activate
which uwsgi

# 為 uwsgi 添加執行權限
sudo chmod +x /home/httpd/django_venv/bin/uwsgi

補充(非必要)

Emperor mode

何謂emperor mode?一般會在大型專案開發時遇到,在這種模式下,uWSGI 啟動一個特殊的 master 進程,稱為"emperor",該進程可以監控一個或多個虛擬主機或應用程序的進程。

這些虛擬主機或應用程序可以是不同的 Python 應用程序,也可以是不同的框架或應用程序。有興趣了解他的功能可以直接去看官方 docs

1
2
3
4
5
6
7
# 去 django_venv 目錄下新增 vassals 資料夾
cd /home/httpd/django_venv
mkdir vassals
# 將 uwsgi.ini 檔 link 至 vassals 下
sudo ln -s /home/httpd/niceshop/uwsgi.ini /home/httpd/django_venv/vassals/
# 用 emperor mode 執行 uWSGI
uwsgi --emperor /home/httpd/django_venv/vassals/ --uid www-data --gid www-data

更改 uwsgi.ini 的 uwsgi-emperor.log 位置

由於原本我的 uwsgi-emperor.log 的位置是跟其他專案一起放在/home/httpd/之下,我覺得有點亂,因此我就想將換到/home/httpd/django_venv/vassals/底下。

1
2
3
4
5
6
7
8
# 編輯 uwsgi.ini
vim /home/httpd/niceshop/uwsgi.ini
# uwsgi.ini 內容
# enable uwsgi master process
.........
vacuum = true
# 將這邊路徑改成新的地方
daemonize = /home/httpd/django_venv/vassals/uwsgi-emperor.log

改完路徑後還沒完,此時直接用 emperor mode 執行 uwsgi 會出現permission denined的問題,代表他沒有權限可以編輯uwsgi-emperor.log檔案。

1
2
3
4
5
6
# 原本的權限狀態
-rw-r----- 1 root root 2.6K 3月 10 09:52 uwsgi-emperor.log
# 更改目錄所有權為 www-data 用戶和組
sudo chown -R www-data:www-data /home/httpd/django_venv/vassals/
# 再次用 ll 指令察看更改後的權限狀態
-rw-r----- 1 www-data www-data 5.2K 3月 10 10:04 uwsgi-emperor.log

這樣做的目的是為了確保 uwsgi 服務可以以 www-data 用戶和組的身份運行,這樣 uwsgi 就可以擁有對/home/httpd/django_venv/vassals/目錄和其子目錄的讀取、寫入和執行權限。此時再執行uwsgi --emperor /home/httpd/django_venv/vassals/ --uid www-data --gid www-data應就能看到預設畫面了


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版