跳轉到

使用 Docker 封裝 Django APP 與部署

開始之前

任務目標

在這個任務中,你將學習:

  • 使用環境變數管理敏感設定
  • 設定 WhiteNoise 來服務靜態檔案
  • 安裝 PostgreSQL 驅動程式
  • 使用 Gunicorn 作為正式環境的 WSGI 伺服器
  • 建立 Docker 映像檔來封裝 Django 專案
  • 理解正式環境部署的最佳實踐

前置需求

在開始之前,請確保你已經安裝 Docker。你可以從 Docker 官網 下載並安裝。

安裝完成後,可以在終端機執行以下指令來確認安裝成功:

docker --version

為什麼需要 Docker?

在開發 Django 專案時,我們經常會遇到「在我的電腦上可以執行」的問題。Docker 透過容器化技術,讓我們能夠將應用程式及其所有相依性打包在一起,確保在任何環境中都能一致地執行。

對於 Django 專案來說,Docker 特別有用,因為:

  • 環境一致性:開發、測試、正式環境使用相同的設定
  • 快速部署:一個指令就能啟動整個應用程式
  • 依賴隔離:不同專案的依賴不會互相衝突
  • 易於擴展:可以輕鬆地水平擴展應用程式

環境變數設定

在正式環境中,我們絕對不能把敏感資訊(如 SECRET_KEY、資料庫密碼)直接寫在程式碼裡。最佳實踐是使用環境變數來管理這些設定。

安裝 environs

environs 是一個方便的 Python 套件,可以幫助我們從環境變數讀取設定值,並提供型別轉換和驗證功能。

首先,安裝 environs[django]

uv add 'environs[django]'

為什麼是 environs[django] 呢?

environs[django] 包含了針對 Django 設定最佳化的額外功能,例如 dj_db_url() 可以直接解析 DATABASE_URL 環境變數。

修改 settings.py

接下來,我們要修改 core/settings.py,讓它能從環境變數讀取設定。

首先,在檔案開頭加入 environs 的 import:

core/settings.py
from pathlib import Path

from django.utils.translation import gettext_lazy as _
from environs import Env

env = Env()
env.read_env()

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

先建立 Env 實例,用來讀取環境變數,接著呼叫 read_env() 會自動尋找專案根目錄的 .env 檔案並載入,不存在也不會失敗。


修改 DEBUG 設定:

core/settings.py
DEBUG = env.bool("DEBUG", default=False)

從環境變數 DEBUG 讀取布林值,預設為 False。這樣在正式環境中,如果忘記設定 DEBUG,至少不會意外開啟除錯模式。


修改 SECRET_KEY 設定:

core/settings.py
SECRET_KEY = env.str("SECRET_KEY")

從環境變數 SECRET_KEY 讀取字串。這個設定是必填的,如果沒有設定會直接報錯。


修改 ALLOWED_HOSTS 設定:

core/settings.py
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])

從環境變數 ALLOWED_HOSTS 讀取列表,預設為空列表。env.list() 會自動將逗號分隔的字串轉換為列表,例如 ALLOWED_HOSTS=localhost,example.com 會變成 ["localhost", "example.com"]


最後,修改 DATABASES 設定:

core/settings.py
DATABASES = {
    "default": env.dj_db_url(
        "DATABASE_URL",
        default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}",
    )
}

dj_db_url()environs[django] 提供的特殊方法,可以直接解析 DATABASE_URL 環境變數,如果沒有設定 DATABASE_URL,預設使用 SQLite 資料庫。

DATABASE_URL 格式

DATABASE_URL 使用類似網址的格式來描述資料庫連線資訊:

  • SQLite: sqlite:///path/to/db.sqlite3
  • PostgreSQL: postgresql://username:password@host:port/database_name
  • MySQL: mysql://username:password@host:port/database_name

建立 .env 檔案(開發用)

為了方便本地開發,我們可以建立一個 .env 檔案:

.env
DEBUG=True
SECRET_KEY=django-insecure-8t080y(vy-sxo&8n*lh@+few+=4*f2lu#ew-i1gb+c3^z-zf-h

不要提交 .env 到版本控制

.env 檔案通常包含敏感資訊,絕對不要提交到 Git。請確保你的 .gitignore 包含 .env

但通常我們會建立一個 .env.example 來跟其他開發者告知開發這個專案需要設定哪些環境變數

現在執行開發伺服器,確認一切正常:

uv run manage.py runserver

WhiteNoise

為什麼需要 WhiteNoise?

在開發環境中,Django 的開發伺服器會自動服務靜態檔案。但在正式環境中,Django 並不擅長服務靜態檔案(Static Files),原因包括:

  1. 效能問題:Django 是用 Python 寫的,服務靜態檔案遠不如專門的 Web 伺服器(如 Nginx)有效率
  2. 安全性:Django 的 runserver 指令明確標示「不適用於正式環境」
  3. 資源佔用:讓 Django 處理靜態檔案會浪費應用伺服器的資源

傳統的解決方案是使用 Nginx 或 Apache 來服務靜態檔案,但這會增加部署的複雜度。WhiteNoise 提供了一個更簡單的解決方案:它是一個 Python middleware,可以讓你的 Django 應用程式直接服務靜態檔案,而且效能接近專門的 Web 伺服器。

WhiteNoise 的優點:

  • 部署簡單,不需要設定額外的 Web 伺服器
  • 自動壓縮靜態檔案(gzip)
  • 支援快取和版本控制
  • 適合中小型專案,但如果你很在乎效能或是你的專案流量很大請務必使用 Nginx 等來服務靜態檔案。

安裝 WhiteNoise

uv add whitenoise

設定 WhiteNoise

core/settings.py 中,將 WhiteNoiseMiddleware 加入 MIDDLEWARE,並且要放在 SecurityMiddleware 之後:

core/settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

WhiteNoise 必須放在 SecurityMiddleware 之後,這樣才能正確處理安全性標頭


接著,在 STATIC_ROOT 設定之後,加入 WhiteNoise 的設定:

core/settings.py
STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

CompressedManifestStaticFilesStorage 會自動壓縮靜態檔案,並在檔名中加入 hash 值,方便做快取控制

現在執行 collectstatic 指令來測試:

uv run manage.py collectstatic

你會看到 WhiteNoise 將靜態檔案收集到 assets/ 目錄,並且自動產生壓縮版本。

安裝 PostgreSQL 驅動程式

雖然 SQLite 在開發環境很方便,但在正式環境中,我們通常會使用更強大的資料庫系統,如 PostgreSQL。即使你現在不打算立即使用 PostgreSQL,提前安裝驅動程式可以讓 Docker 映像檔更有彈性。

安裝 psycopg(PostgreSQL 的 Python 驅動程式):

uv add 'psycopg[binary]'

psycopg vs psycopg2

psycopg(又稱 psycopg3)是新一代的 PostgreSQL 驅動程式,效能更好、支援更多功能。如果你看到其他教學使用 psycopg2,那是舊版本。

[binary] 表示安裝預先編譯的二進位版本,安裝速度較快。

安裝 Gunicorn

Django 的開發伺服器(runserver)只適合開發使用,在正式環境中,我們需要一個正式的 WSGI 伺服器。

Gunicorn(Green Unicorn)是一個流行的 Python WSGI HTTP 伺服器,特點包括:

  • 輕量且高效能
  • 支援多個 worker process,可以處理並發請求
  • 設定簡單
  • 廣泛用於正式環境

安裝 Gunicorn:

uv add gunicorn

你可以測試執行 Gunicorn:

uv run gunicorn core.wsgi --workers 4 --bind 0.0.0.0:8000
  • core.wsgi:指向我們專案的 WSGI 應用程式
  • --workers 4:使用 4 個 worker process
  • --bind 0.0.0.0:8000:監聽所有網路介面的 8000 埠

Worker 數量該如何設定?

一個常見的經驗法則是:worker 數量 = (2 × CPU 核心數) + 1

例如,如果你的伺服器有 2 個 CPU 核心,可以設定 5 個 workers。

現在可以按 Ctrl+C 停止 Gunicorn,我們接下來會在 Docker 中使用它。

建立 Docker 映像檔

現在我們已經安裝了所有必要的套件,接下來要建立 Docker 映像檔來封裝整個專案。

建立 Dockerfile

在專案根目錄建立 Dockerfile

Dockerfile
FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim

WORKDIR /app

COPY pyproject.toml uv.lock ./

ENV UV_NO_SYNC=1

RUN \
    apt-get update && \
    apt-get install -y gettext && \
    \
    uv sync --no-default-groups --locked && \
    \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

COPY . .

COPY --chmod=755 run-server ./

EXPOSE 8000

CMD [ "/app/run-server" ]

Dockerfile 重點:

  1. 使用 Astral 官方的 uv 映像檔,基於 Python 3.14 和 Debian Trixie
  2. 設定工作目錄為 /app
  3. 先複製依賴定義檔案,利用 Docker 的層級快取,當依賴沒變動時可以加快建置速度
  4. 設定環境變數,告訴 uv 不要自動同步虛擬環境
  5. 安裝
    1. 更新 apt 套件列表,並安裝 gettext 工具,用於編譯翻譯檔案(compilemessages)
    2. 使用 uv 安裝專案依賴,--no-default-groups 表示不安裝開發用的依賴群組,--locked 確保使用 lock 檔案中的版本
    3. 清理 apt 快取,減少映像檔大小
  6. 複製所有專案檔案到容器中
  7. 複製啟動腳本,並設定可執行權限
  8. 聲明服務會跑在哪個 port 上
  9. 設定容器啟動時執行的指令

建立 .dockerignore

.dockerignore 用來指定哪些檔案不要複製到 Docker 映像檔中:

.dockerignore
# Ignore all files
*

# Allow list

## uv
!pyproject.toml
!uv.lock

## Python files
!**/*.py

## Translation files
!**/*.po

## Static files
!**/*.html
!**/*.css
!**/*.js
!static/

## Locale files
!locale/

## Scripts
!run-server

# But not these files
grid_demo.html

這個 .dockerignore 採用「預設拒絕,明確允許」的策略,可以確保不會意外把不需要的檔案打包進去。

建立 run-server 腳本

在專案根目錄建立 run-server 腳本,這是容器啟動時會執行的入口點:

run-server
#!/bin/bash

set -euo pipefail  # (1)!

# Run migrations
uv run manage.py migrate  # (2)!

# Collect static files
uv run manage.py collectstatic --noinput  # (3)!

# If DJANGO_SUPERUSER_USERNAME, DJANGO_SUPERUSER_EMAIL and DJANGO_SUPERUSER_PASSWORD are set, create a superuser
if [ -n "${DJANGO_SUPERUSER_USERNAME:-}" ] && [ -n "${DJANGO_SUPERUSER_EMAIL:-}" ] && [ -n "${DJANGO_SUPERUSER_PASSWORD:-}" ]; then  # (4)!
    uv run manage.py createsuperuser --noinput || true
fi

# Compile messages
uv run manage.py compilemessages  # (5)!

# Run server
uv run gunicorn core.wsgi --workers 4 --bind 0.0.0.0:8000  # (6)!
  1. 設定 bash 錯誤處理:-e 遇到錯誤立即停止,-u 使用未定義變數時報錯,-o pipefail 管線中任何指令失敗就返回失敗
  2. 執行資料庫遷移,確保資料庫結構是最新的
  3. 收集靜態檔案到 STATIC_ROOT--noinput 表示不要詢問確認
  4. 如果環境變數有設定超級使用者的資訊,自動建立超級使用者,如果建立失敗也沒關係
  5. 編譯翻譯訊息檔案
  6. 啟動 Gunicorn 伺服器

建置 Docker 映像檔

現在可以建置 Docker 映像檔了:

docker build -t django-playground .

指令說明

  • docker build:建置 Docker 映像檔
  • -t django-playground:為映像檔命名為 django-playground
  • .:使用當前目錄作為建置上下文

建置完成後,可以查看映像檔:

docker images django-playground

執行 Docker 容器

建立一個 .env.docker 檔案,用於 Docker 容器的環境變數:

.env.docker
DEBUG=False
SECRET_KEY=your-secret-key-change-this-in-production
ALLOWED_HOSTS=*
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_EMAIL=admin@example.com
DJANGO_SUPERUSER_PASSWORD=password

正式環境的 SECRET_KEY

在正式環境中,請務必更換成一個隨機、安全的 SECRET_KEY。你可以使用以下 Python 指令來產生:

from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())

如果有安裝 django-extensions 的話可以使用下方指定

uv run manage.py generate_secret_key

現在可以執行容器:

docker run --rm -it -p 8888:8000 --env-file .env.docker django-playground

指令說明

  • docker run:執行容器
  • --rm:容器停止後自動刪除
  • -p 8000:8000:將容器的 8000 埠對應到主機的 8888 埠
  • --env-file .env.docker:從檔案載入環境變數
  • django-playground:要執行的映像檔名稱

現在開啟瀏覽器,造訪 http://localhost:8888,你應該會看到你的 Django 應用程式正在執行!

如何進入容器進行除錯?

如果需要進入容器查看檔案或執行指令,可以使用:

docker run --rm -it --env-file .env.docker django-playground bash

這會啟動容器並開啟一個互動式的 bash shell。

使用 PostgreSQL 資料庫

如果你想在 Docker 中使用 PostgreSQL,可以修改 .env.docker

.env.docker
DEBUG=False
SECRET_KEY=your-secret-key-change-this-in-production
ALLOWED_HOSTS=*
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_EMAIL=admin@example.com
DJANGO_SUPERUSER_PASSWORD=password
DATABASE_URL=postgresql://postgres:postgres@db:5432/django_playground

然後使用 Docker Compose 來同時啟動 Django, PostgreSQL 和 Adminer(資料庫管理工具):

docker-compose.yml
name: django

services:
  adminer:
    image: adminer:5
    restart: always
    ports:
      - 9999:8080
  db:
    image: postgres:18
    environment:
      POSTGRES_DB: django_playground
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql

  web:
    image: django-playground
    ports:
      - 8888:8000
    env_file:
      - .env.docker
    depends_on:
      - db

volumes:
  postgres_data:

啟動服務背景:

docker compose up -d

看 log

docker compose logs -f

停止服務:

docker compose down

任務結束

完成!

恭喜你完成了這個任務!現在你已經學會:

  • 使用環境變數管理敏感設定
  • 設定 WhiteNoise 來服務靜態檔案
  • 安裝 PostgreSQL 驅動程式
  • 使用 Gunicorn 作為正式環境的 WSGI 伺服器
  • 建立 Docker 映像檔來封裝 Django 專案
  • 理解正式環境部署的最佳實踐

你的 Django 專案現在已經準備好部署到任何支援 Docker 的平台了!