Python

Ubuntu Linux 以 Django 搭配 PostgreSQL、Nginx、Gunicorn 開發布署教學與範例

介紹如何使用 Django 搭配 PostgreSQL 資料庫、Nginx 與 Gunicorn 網頁伺服器,開發與布署基礎的網頁應用程式。

安裝必要套件

這裡我們選擇採用 Python 3 的環境進行開發,搭配 PostgreSQL 資料庫、Nginx 網頁伺服器,這些相關套件可透過 apt 安裝:

# 安裝 Pyton3、PostgreSQL、Nginx、curl 相關套件
sudo apt install python3-pip python3-dev libpq-dev \
  postgresql postgresql-contrib nginx curl

至於 Gunicorn 會在後續的開發過程中透過 pip 安裝。

設定 PostgreSQL 資料庫

進入 PostgreSQL 資料庫的 psql 互動式指令操作環境:

# 進入 PostgreSQL 資料庫操作環境
sudo -u postgres psql

在 PostgreSQL 資料庫中,建立一個專門用於 Django 專案的資料庫(這裡我們將此資料庫命名為 myproject):

-- 建立 myproject 資料庫
CREATE DATABASE myproject;

接著建立一個專案專用的 PostgreSQL 資料庫帳號(帳號名稱取為 myprojectuser):

-- 建立 myprojectuser 使用者帳號
CREATE USER myprojectuser WITH PASSWORD 'password';

其中 password 是設定的密碼,可自由更換。

以下是一些預設的 PostgreSQL 資料庫帳號連線設定,設定好這些設定可以加速資料庫的操作。

-- 設定預設編碼
ALTER ROLE myprojectuser SET client_encoding TO 'utf8';

-- 設定預設 transaction isolation 規則
ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed';

-- 設定時區
ALTER ROLE myprojectuser SET timezone TO 'Asia/Taipei';

Django 預設的編碼是採用 UTF8,所以這裡將 PostgreSQL 的 client_encoding 也設定為 utf8,而時區的部分 Django 預設是 UTC,建議改為自己所在的時區,以台灣來說就是 Asia/Taipei

最後授予 myprojectuser 使用者在 myproject 資料庫上的所有權限:

-- 授予 myprojectuser 使用者在 myproject 資料庫上的所有權限
GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser;

建立 Python 虛擬環境

先將 pip 更新至最新版之後,安裝 virtualenv

# 更新 pip 至最新版本
sudo -H pip3 install --upgrade pip

# 安裝 virtualenv
sudo -H pip3 install virtualenv

建立一個 Django 專案目錄 myprojectdir,並在專案目錄中建立一個專案用的 Python 虛擬環境:

# 建立專案目錄
mkdir ~/myprojectdir
cd ~/myprojectdir

# 建立 Python 虛擬環境
virtualenv myprojectenv

啟用新建的 Python 虛擬環境:

# 啟用 Python 虛擬環境
source myprojectenv/bin/activate

在專用的 Python 虛擬環境中,使用 pip 安裝 Django、Gunicorn 與 PostgreSQL 相關的 Python 模組:

# 安裝 Django、Gunicorn 與 PostgreSQL 相關的 Python 模組
pip install django gunicorn psycopg2-binary

建立 Django 專案

在剛建立好的專案目錄 ~/myprojectdir 中,建立一個新的 Django 專案 myproject

# 建立 Django 專案
django-admin startproject myproject ~/myprojectdir

此時 ~/myprojectdir 目錄中會包含以下三個項目:

  • ~/myprojectdir/manage.py:Django 專案管理用的指令稿。
  • ~/myprojectdir/myproject/:Django 專案套件目錄,內含 __init__.pysettings.pyurls.pyasgi.py 與 wsgi.py 這幾個檔案。
  • ~/myprojectdir/myprojectenv/:之前建立的 Python 虛擬環境。

Django 專案建立好之後,首先要修改 ~/myprojectdir/myproject/settings.py 這個 Django 專案的設定檔。Django 預設所使用的資料庫是 SQLite,這裡我們要將 SQLite 改為 PostgreSQL,將上面建立好的 PostgreSQL 資料庫、帳號與密碼填入其中,而這裡 PostgreSQL 的轉接器(adaptor)我們採用剛剛以 pip 安裝的 psycopg2

# PostgreSQL 資料庫連線設定
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'myproject',      # 資料庫名稱
        'USER': 'myprojectuser',  # 帳號
        'PASSWORD': 'password',   # 密碼
        'HOST': 'localhost',      # 資料庫主機
        'PORT': '',               # 連接埠(預設為 5432)
    }
}

Django 可以透過 ALLOWED_HOSTS 設定 Django 應用程式允許提供服務的主機或網域名稱(也就是瀏覽器網址列上的呈現的主機位址),以下是一些設定範例:

# 設定允許提供服務的主機名稱與 IP 位址
ALLOWED_HOSTS = [ 'example.com', '192.168.1.2']

# 設定允許提供服務的網域、子網域與 IP 位址
ALLOWED_HOSTS = [ '.example.com', '192.168.1.2']

# 設定允許提供服務的 IP,並開放本機存取權限
ALLOWED_HOSTS = [ '192.168.1.2', '.localhost', '127.0.0.1', '[::1]']

設定 Django 專案的靜態檔案網址以及布署路徑:

# 靜態檔案網址
STATIC_URL = '/static/'

# 靜態檔案布署路徑
STATIC_ROOT = BASE_DIR / 'static'

設定使用中文語系,以及台北時區:

# 設定中文語系
LANGUAGE_CODE = 'zh-Hant'

# 設定台北時區
TIME_ZONE = 'Asia/Taipei'

初始化資料庫

建立 Django 的資料庫遷移(migration)指令稿,並執行資料庫遷移:

# 建立資料庫遷移(migration)指令稿
~/myprojectdir/manage.py makemigrations

# 執行資料庫遷移
~/myprojectdir/manage.py migrate

建立 Django 專案管理者帳號:

# 建立 Django 專案管理者帳號
~/myprojectdir/manage.py createsuperuser

建立管理者帳號時,要輸入帳號名稱與 Email,並設定密碼。

Django 靜態檔案

在正式的服務環境中,會將靜態檔案與動態的 Python 指令稿分開處理,通常動態的 Python 指令稿會讓 Python 的 WSGI 網頁伺服器(例如 Gunicorn)處理,而靜態檔案則交給普通的網頁伺服器(例如 Nginx)。

Django 應用程式開發完成之後,可以執行以下指令將所有應用程式中的靜態(static)檔案都布署至 STATIC_ROOT 所指定目錄之下:

# 布署靜態檔案
~/myprojectdir/manage.py collectstatic

實際上這個靜態檔案布署指令是在 Django 專案開發完成之後,在布署階段才需要執行的,此處是先進行測試。

測試 Django 應用程式

執行 Django 測試用的內建網頁伺服器,讓網頁伺服器開在每一個網路介面的 8000 連接埠:

# 啟動 Django 網頁伺服器
~/myprojectdir/manage.py runserver 0.0.0.0:8000

接著看看是否可以正常開啟 http://server_domain_or_IP:8000/ 這個網址,其中 server_domain_or_IP 就換成自己伺服器的主機名稱或 IP 位址。(此處的 server_domain_or_IP 記得要加入 ALLOWED_HOSTS 中)

若可以正常看到 Django 專案預設畫面,就表示一切正常,接著就可以按下 Ctrl + c 停止 Django 內建的網頁伺服器。

測試 Gunicorn 伺服器

設定好 Django 應用程式之後,要測試 Django 應用程式是否可以正常在 Gunicorn 伺服器中運行。

在 Django 應用程式中,預設就有提供最基本的 WSGI 設定檔 myproject/wsgi.py,這個檔案也是一個 Python 模組,其中建立的 application 變數就是 Gunicorn 伺服器跟應用程式溝通的管道,Gunicorn 伺服器可以直接透過這個設定檔來運行 Django 應用程式:

# 使用 Gunicorn 伺服器運行 Django 應用程式
cd ~/myprojectdir
gunicorn --bind 0.0.0.0:8000 myproject.wsgi

執行之後的效果會跟 Django 內建伺服器類似,但是在這裡使用 Gunicorn 伺服器時,靜態(static)檔案會出現找不到的問題,這是正常的。這裡我們同樣檢查 http://server_domain_or_IP:8000/ 這個頁面,除了靜態檔案之外是否都可以正常運作,若正常就表示沒問題。

測試完成後,就可以按下 Ctrl + c 停止 Gunicorn 網頁伺服器。最後離開 Python 虛擬環境:

# 離開 Python 虛擬環境
deactivate

設定 systemd 系統服務

在設定與測試完 Django 應用程式與 Gunicorn 網頁伺服器之後,我們接著要設定讓系統的 systemd 可以自動管理 Django 應用程式與 Gunicorn 網頁伺服器的啟動、運行與停止。

這裡我們將建立一個 Gunicorn 的 socket 設定檔,以及另一個 Gunicorn 的 service 設定檔,在此設定之下,systemd 會開啟一個 UNIX socket 傾聽 Gunicorn 的請求,當收到請求的時候,就自動啟動 Gunicorn 伺服器來進行處理。

新增 /etc/systemd/system/gunicorn.socket 設定檔,填入以下內容:

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# 只有 Nginx 服務會需要存取此 socket
SocketUser=www-data

# 更嚴格限制 socket 權限
SocketMode=600

[Install]
WantedBy=sockets.target

新增 /etc/systemd/system/gunicorn.service 設定檔,填入以下內容:

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify

# 執行服務的使用者與群組
User=myuser
Group=www-data

# 可寫入的執行期間目錄(位於 /run/ 之下)
RuntimeDirectory=gunicorn

# 工作目錄
WorkingDirectory=/home/myuser/myprojectdir

# 服務執行指令
ExecStart=/home/myuser/myprojectdir/myprojectenv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          myproject.wsgi:application

# 重新載入指令
ExecReload=/bin/kill -s HUP $MAINPID

# 服務終止模式
KillMode=mixed

# 停止服務等待時間
TimeoutStopSec=5

# 是否有獨立的暫存空間
PrivateTmp=true

[Install]
WantedBy=multi-user.target

設定好 gunicorn.socketgunicorn.service 設定檔之後,啟用 gunicorn.socket 服務,並將其設定為開機自動啟動:

# 立即啟動、同時設定開機自動啟動 gunicorn.socket
sudo systemctl enable --now gunicorn.socket

啟動 gunicorn.socket 服務之後,檢查該服務的狀態:

# 檢查 gunicorn.socket 服務狀態
systemctl status gunicorn.socket

若要查看 gunicorn.socket 服務的記錄(Gunicorn 伺服器的輸出訊息),可以使用 journalctl 指令查看:

# 查看 gunicorn.socket 服務記錄
journalctl -u gunicorn.socket

我們可以使用 curl 發送測試的 HTTP 請求,看看 gunicorn.service 服務是否有正常被啟動:

# 測試 Gunicorn 網頁伺服器
sudo -u www-data curl --unix-socket /run/gunicorn.sock localhost

正常來說,systemd 收到 HTTP 請求之後,會自動啟動 gunicorn.service 來處理 HTTP 請求,所以執行之後會看到正常的 HTML 網頁原始碼。

我們可以接著檢查 gunicorn.service 服務的狀態:

# 檢查 gunicorn.service 服務狀態
systemctl status gunicorn.service

正常來說,這時候的 gunicorn.service 服務是處於運行狀態的。

如果在執行 curl 或查看 gunicorn.service 服務狀態出現問題,可以使用 journalctl 指令查看 gunicorn.service 服務記錄:

# 查看 gunicorn.service 服務記錄
journalctl -u gunicorn.service

如果在除錯的過程中,修改了 gunicorn.service 設定檔內容,就要讓 systemd 重新載入設定檔,再重新啟動 gunicorn.service 服務:

# 讓 systemd 重新載入設定檔
sudo systemctl daemon-reload

# 重新啟動 gunicorn.service
sudo systemctl restart gunicorn

Nginx 網頁伺服器

Gunicorn 伺服器設定完畢之後,最後一步就是設定 Nginx 網頁伺服器,讓外部的連線導入 Gunicorn 伺服器。

新增一個 Nginx 網站設定檔 /etc/nginx/sites-available/myproject,內容如下:

# Django 應用程式
server {
    # 傾聽埠號
    listen 80;
    listen [::]:80;

    # 伺服器主機名稱
    server_name server_domain_or_IP;

    # 忽略找不到 favicon.ico 的錯誤
    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }

    # 靜態檔案路徑
    location /static/ {
        root /home/myuser/myprojectdir;
    }

    # 動態網頁
    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

建立一個連結檔,將 /etc/nginx/sites-available/myproject 連結至 /etc/nginx/sites-enabled 目錄之下:

# 建立連結檔
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled

測試 Nginx 設定檔,看看是否有錯誤:

# 測試 Nginx 設定檔
sudo nginx -t

重新啟動 Nginx 網頁伺服器服務:

# 重新啟動 Nginx 網頁伺服器服務
sudo systemctl restart nginx

這樣就完成 Nginx 與 Gunicorn 伺服器的整合設定了。

以 Certbot 安裝 SSL 憑證

若希望在 Nginx 網頁伺服器上加裝 SSL 憑證,採用 HTTPS 的加密傳輸,可以在設定好主機的 DNS 正解記錄之後,透過 Certbot 安裝免費的 SSL 憑證。

參考 Certbot 官方網站上的安裝步驟,首先將 snap 更新至最新版:

# 更新 snap 至最新版
sudo snap install core
sudo snap refresh core

透過 snap 安裝 certbot

# 安裝 certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

透過 certbot 取得並安裝 SSL 憑證至 Nginx 伺服器:

# 取得並安裝憑證至 Nginx 伺服器
sudo certbot --nginx

這樣就完成整套的 Django 應用程式運行環境的建置與布署了。若要更進一步停用不安全的 TLS 1.1 與 TLS 1.0 協定,只使用 TLS 1.2 與 TLS 1.3 協定,可以參考 Ubuntu Linux 設定 Nginx 使用 TLS 1.2 與 1.3 教學與範例

Ubuntu 防火牆

若 Ubuntu Linux 有啟用 UFW 防火牆,在開發與測試階段,可以執行以下指令開啟 Django 所使用的 TCP 連接埠,預設的的埠號為 8000

# 開啟 Ubuntu 防火牆 TCP 的 8000 連接埠
sudo ufw allow 8000/tcp

或是直接將 UFW 防火牆暫時關閉:

# 停用 Ubuntu 防火牆
sudo ufw disable

當開發與測試完成後,可恢復防火牆設定:

# 刪除 Ubuntu 防火牆規則
sudo ufw delete allow 8000/tcp

# 啟用 Ubuntu 防火牆
sudo ufw enable

而在正式服務階段,只有 Nginx 網頁伺服器會直接接受外部的連線,所以只需要開啟 Nginx 網頁伺服器所需的防火牆設定:

# 開啟 Nginx 網頁伺服器用的防火牆
sudo ufw allow 'Nginx Full'

記錄檔

若需要將 Gunicorn 的紀錄訊息紀錄至 /var/log/ 之下,可以修改 /etc/systemd/system/gunicorn.service 設定檔,將服務執行指令加上記錄檔位置,並設定 PID 檔案位置:

# 服務執行指令
ExecStart=/home/myuser/myprojectdir/myprojectenv/bin/gunicorn \
          --access-logfile /var/log/gunicorn/access.log \
          --error-logfile /var/log/gunicorn/error.log \
          --pid /var/run/gunicorn/gunicorn.pid \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          myproject.wsgi:application

接著建立放置記錄檔的目錄,並設定權限:

# 建立放置記錄檔的目錄
mkdir /var/log/gunicorn

# 設定權限
chown myuser:www-data /var/log/gunicorn/

然後重新載入 systemd 設定檔,並重新啟動 gunicorn.service 服務:

# 讓 systemd 重新載入設定檔
sudo systemctl daemon-reload

# 重新啟動 gunicorn.service
sudo systemctl restart gunicorn

最後新增一個 /etc/logrotate.d/gunicorn 設定檔,內容如下:

/var/log/gunicorn/*.log {
    weekly
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 0640 health www-data
    postrotate
        kill -USR1 $(cat /var/run/gunicorn/gunicorn.pid)
    endscript
}

參考資料

Share
Published by
Office Guide

Recent Posts

Python 使用 PyAutoGUI 自動操作滑鼠與鍵盤

本篇介紹如何在 Python ...

1 年 ago

Ubuntu Linux 以 WireGuard 架設 VPN 伺服器教學與範例

本篇介紹如何在 Ubuntu ...

1 年 ago

Linux 網路設定 ip 指令用法教學與範例

本篇介紹如何在 Linux 系...

1 年 ago

Linux 以 Cryptsetup、LUKS 加密 USB 隨身碟教學與範例

介紹如何在 Linux 系統中...

1 年 ago