[Django] 佈署Django至Raspberry Pi的Apache伺服器

這篇文章是在完成MDN的Django課程後,將Django專案佈署到Raspberry Pi 3 B+的Apache伺服器的步驟紀錄;如果有任何疑問、錯誤,歡迎留言討論。

Environment

DevelopmentDeployment
  • Windows 10 ver 1909
  • Python ver 3.8.3
  • Django ver 3.0.7
  • virtualenv ver 20.0.23
  • whitenoise ver 5.1.0
  • Raspberry Pi 4.19.118-v7+
  • apache2 ver 2.4.38-3+deb10u3
  • libapache2-mod-wsgi-py3 ver 4.6.5-1
  • Python ver 3.7.3
  • Django ver 3.0.7
  • virtualenv ver 20.0.23
  • whitenoise ver 5.1.0

Directory Structure

下圖是開發環境與佈署環境的專案目錄結構,圖中並沒有將所有檔案列出來,只有配合文章列出一些定位用的檔案;因為在開發環境與佈署環境中,目錄結構有一些變動,所以加上紅色斜體字輔助說明資料夾的用途。
django%2Bfolder%2Btree.png-佈署Django至Raspberry Pi的Apache伺服器

Deploy Step

在開始佈署前,Django本身有個很好用的deployment checklist,可以檢查整個專案中重要的設定,包括安全性、效能、以及操作性,尤其當網站是要佈署到網際網路時,要特別留意安全性的部分;在開發環境下輸入這行指令來檢查專案的設定。
python3 manage.py check --deploy

Step 1

首先要在佈署環境上安裝virtualenv,然後建立並啟動一個虛擬環境;Raspberry Pi本身有Python 2.7與3.7兩個版本,安裝時要安裝在專案使用的版本中;這邊是使用3.7版本,所以以pip3來安裝。
pi@raspberrypi:~ $ sudo pip3 install virtualenv
pi@raspberrypi:~ $ cd DjangoProject
pi@raspberrypi:~/DjangoProject $ virtualenv -p python3 venv
pi@raspberrypi:~/DjangoProject $ source venv/bin/activate

使用虛擬環境的原因應該不用多作說明,就算佈署環境目前只有執行這個Django專案,但難以保證將來不會有其他的專案執行,無論如何保持每個專案的環境獨立這算是Python開發的常識;文章後面也會看到當Apache要執行Django專案時,也是使用虛擬環境來執行。

Step 2

為了確保之後pip會將套件安裝在虛擬環境而不是系統中,可以在.bachrc中加入環境變數來限制使用pip的條件。
(venv) pi@raspberrypi:~/DjangoProject $ vi ~/.bashrc

開啟.bachrc後,加入PIP_REQUIRE_VIRTUALENV環境變數並設定為True,這個環境變數可以讓使用者不在虛擬環境的時候,無法使用pip指令。
export PIP_REQUIRE_VIRTUALENV=true

設定完畢後便可以開始安裝Django與相關套件。
(venv) pi@raspberrypi:~/DjangoProject $ pip3 install django

Step 3

Python的環境建立完成後,便可將專案從開發環境複製到佈署環境,並且用Django的runserver測試專案是否一切正常,有沒有缺少任何必要的檔案或著套件,專案網頁是否能跟開發環境一樣可以順利操作。
(venv) pi@raspberrypi:~/DjangoProject $ cd locallibrary
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ python3 manage.py runserver

如果佈署環境的系統是沒有桌面的版本,可以用curl把網頁抓回來確認是否正常,這邊要注意的是如果在專案中有使用重新導向(Redirect)之類的功能時,使用curl必須輸入導向後的網頁,否則只會抓到HTTP 302錯誤的網頁。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ curl http://127.0.0.1:8000/catalog/

Step 4

接著要為佈署環境修改settings.py與wsgi.py這兩個檔案,但在這之前需要先安裝whitenoise這個middleware套件。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ pip3 install whitenoise

middleware套件簡單來說就是Django在 request與response之間,處理特定用途的plugin,Django本身已經有內建很多middleware來處理如session、csrf token、auth等用途,有興趣的可以看Django 3.0.3 middleware介紹這段影片。

whitenoise則是用來處理在佈署環境中的靜態文件的middleware套件,當settings.py的DEBUG為True時,Django會有自己的機制來處理靜態文件;但是在佈署環境中,DEBUG為了安全性的考量是False時,就必須依賴其他如whitenoise的方式來提供靜態資源服務。

Step 5

安裝完成後,首先來修改佈署環境中的settings.py。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ vi locallobrary/settings.py

依照Django官方的建議,將原本寫死的SECRET_KEY註解掉,改為讀取環境變數來設定SECRET_KEY。
# SECRET_KEY = 'aog^j=6=rrlw0oe0n0(x@cvawoo5f5-%5vl&sq!a@@lx7a6(_o'
SECRET_KEY = os.environ['SECRET_KEY']

或者可以將SECRET_KEY存放在檔案中,再讀取出來設定SECRET_KEY。
with open('/etc/secret_key.txt') as f:
SECRET_KEY = f.read().strip()

同樣依照Django的建議,為了安全性會在佈署環境關閉debug模式,同樣註解掉原本的DEBUG,改為由環境變數來設定;注意True是字串而不是布林值,因為os.environ['DJANGO_DEBUG']的回傳值是字串,如果日後想透過修改環境變數來開關debug模式,而True是布林值的話,那比對的結果永遠都是False。
# DEBUG = True
DEBUG = os.environ['DJANGO_DEBUG'] == 'True'

在ALLOWED_HOSTS設定允許訪問的主機,因為沒有特別的需求,設定為所有主機都可以訪問。
ALLOWED_HOSTS = ['*']

在MIDDLEWARE中將剛剛安裝的whitenoise加入設定,以啟用whitenoise套件。
MIDDLEWARE = [
    ...
    'whitenoise.middleware.WhiteNoiseMiddleware',
]

最後設定STATIC_ROOT,用來收集靜態文件並且作為將來伺服器存取靜態文件的位置。
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

Step 6

接下來要修改佈署環境中的wsgi.py。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ vi locallobrary/wsgi.py

修改的內容是將專案的路徑加入sys.path讓Python直譯器知道要去哪搜尋模組。
import sys
project_home = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_home not in sys.path:
    print(project_home)
    sys.path.append(project_home)

Step 7

剛才在settings.py中SECRET_KEY與DEBUG都要依靠讀取環境變數來設定,可以將這兩個環境變數在/ect/profile設定為全域環境變數,或者在~/.profile設定為使用者環境變數。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ vi /ect/profile
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ vi ~/.profile

開啟檔案後在檔案中加入這兩行,SECRET_KEY的值請依照自己專案的內容修改。
export DJANGO_DEBUG=False
export SECRET_KEY="aog^j=6=rrlw0oe0n0(x@cvawoo5f5-%5vl&sq!a@@lx7a6(_o"

關於debug模式個人的一點心得,在佈署專案的時期還是有可能遇到很多錯誤,這時候以debug模式的資訊來除錯會比Apache的錯誤紀錄還來的方便;但是不知道為什麼就算將環境變數的DJANGO_DEBUG設定為True,也確認settings.py裡面的DEBUG的值是True,網站還是不會顯示debug資訊,除非直接將settings.py裡面的DEBUG設定為True才可以。

Step 8

之前在settings.py中設定了STATIC_ROOT,作為存放靜態文件的位置,Django的collectstatic指令會在當前的位置建立一個與STATIC_ROOT設定相同,名為staticfiles的資料夾,並將專案中所有靜態文件複製到該資料夾中。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ python manage.py collectstatic

Step 9

專案中有些資料夾不會一起複製到佈署環境中,例如在開發環境中測試來讓使用者上傳文件的資料夾,也有些資料夾是只存在於佈署環境中,例如記錄網站日誌的資料夾,接下來就要為網站建立這些將來會需要的資料夾;以media與logs兩個資料夾為例,media是讓使用者上傳文件用的,logs資料夾則是給Apache紀錄伺服器運行日誌用的。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ mkdir media logs

Step 10

Django大致上設定完畢之後,接著開始建立Apache伺服器,首先先安裝Apache套件與讓Apache可以使用WSGI的相關模組。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ sudo apt-get install apache2 libapache2-mod-wsgi-py3

Step 11

安裝完成後接著設定Apache,Apache虛擬主機的設定檔存放在/etc/apache2/sites-available,預設會有一個000-default.conf,建議不要直接修改這個檔案,而是把這個檔案作為樣板複製一份新的設定檔進行修改。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ cd /etc/apache2/sites-available
(venv) pi@raspberrypi:/etc/apache2/sites-available $ sudo cp 000-default.conf mydjangosite.conf
(venv) pi@raspberrypi:/etc/apache2/sites-available $ sudo vi mydjangosite.conf

開啟設定檔後加入以下內容。
<VirtualHost *:80>
    WSGIScriptAlias / /home/pi/DjangoProject/locallibrary/locallibrary/wsgi.py
    WSGIDaemonProcess mydjangoproject.com python-home=/home/pi/DjangoProject/vnev python-path=/home/pi/DjangoProject/locallibrary
    WSGIProcessGroup mydjangoproject.com

    DocumentRoot "/home/pi/DjangoProject/locallibrary"

    Alias /static/ /home/pi/DjangoProject/locallibrary/staticfiles/

    Alias /media/ /home/pi/DjangoProject/locallibrary/media/

    ErrorLog "/home/pi/DjangoProject/locallibrary/logs/error_log"
    CustomLog "/home/pi/DjangoProject/locallibrary/logs/access_log" common

    <Directory "/home/pi/DjangoProject/locallibrary/locallibrary ">
          Require all granted
    </Directory>

    <Directory "/home/pi/DjangoProject/locallibrary/staticfile">
          Require all granted
    </Directory>

    <Directory "/home/pi/DjangoProject/locallibrary/media">
          Require all granted
    </Directory>
</VirtualHost>

WSGIScriptAlias、WSGIDaemonProcess、WSGIProcessGroup的詳細說明可以參考mod_wsgi的官方網站這邊只需要注意WSGIDaemonProcess與WSGIProcessGroup後面的mydjangoproject.com要設定為相同的名稱即可。

另外要注意Alias /static/ /home/pi/DjangoProject/locallibrary/staticfiles與<Directory "/home/pi/DjangoProject/locallibrary/staticfiles">這兩行,路徑名稱與使用之前collectstatic指令建立的靜態文件路徑一樣。

Step 12

接著修改Apache的ports.conf,將專案虛擬主機的埠口與Apache監聽的埠口綁定。
(venv) pi@raspberrypi:/etc/apache2/sites-available $ vi etc/apache2/ports.conf

透過NamevirtualHost加入專案虛擬主機設定檔中VirtualHost的埠口設定,並且把Apache的監聽埠口也修改為同樣埠口。
NamevirtualHost *:80
Listen 80

Step 13

建立apache相關的設定檔之後,必須停用預設的設定檔,以及啟用並載入新的設定檔;當前啟用的設定檔,會在/etc/apache2/sites-enabled中建立連結檔,可以透過這個方式確認目前啟用的設定檔。
(venv) pi@raspberrypi:/etc/apache2/sites-available $ sudo a2ensite mydjangosite.conf
(venv) pi@raspberrypi:/etc/apache2/sites-available $ sudo service apache2 reload
(venv) pi@raspberrypi:/etc/apache2/sites-available $ sudo a2dissite 000-default.conf

Step 14

專案網站相關的設定大致上都完成了,最後來修改系統中專案資料夾的權限,確保專案網站可以正確而且安全的使用;對於整個專案,將文件的權限設定為644,資料夾設定為755。
(venv) pi@raspberrypi:/etc/apache2/sites-available $ cd ~/DjangoProject
(venv) pi@raspberrypi:~/DjangoProject $ sudo chmod -R 644 locallibrary
(venv) pi@ aspberrypi:~/DjangoProject $ sudo find locallibrary -type d | xargs chmod 755

media資料夾是讓使用者上傳文件的資料夾,所以要讓Apache對資料夾有寫入的權限。
(venv) pi@raspberrypi:~/DjangoProject $ cd locallibrary
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ sudo chgrp -R www-data media
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ sudo chmod -R g+w media

專案使用的sqlite3的資料庫同樣也必須要讓Apache有寫入的權限。
(venv) pi@raspberrypi:~/DjangoProject/locallibrary $ cd ~/DjangoProject
(venv) pi@raspberrypi:~/DjangoProject $ sudo chgrp www-data locallibrary
(venv) pi@raspberrypi:~/DjangoProject $ sudo chmod g+w locallibrary
(venv) pi@raspberrypi:~/DjangoProject $ sudo chgrp www-data locallibrary/db.sqlite3
(venv) pi@raspberrypi:~/DjangoProject $ sudo chmod g+w locallibrary/db.sqlite3

Step 15

完成所有設定之後重新啟動apache服務,透過其他主機的瀏覽器,便可以由Raspberry Pi的IP位址與之前設定的埠口看到專案網站了。
(venv) pi@raspberrypi:~/DjangoProject $ sudo systemstrl restart apache2

留言