IT策士? 10余年一線大廠經(jīng)驗(yàn),專注 IT 思維、架構(gòu)、職場進(jìn)階。我會(huì)在公眾號(hào)、今日頭條持續(xù)發(fā)布最新文章,助你少走彎路。
在本地跑得好好的 Django,一上服務(wù)器就各種報(bào)錯(cuò)?
環(huán)境不一致、依賴缺失、進(jìn)程守護(hù)……傳統(tǒng)部署的坑數(shù)不勝數(shù)。
本文將帶你用 Gunicorn + Nginx + Docker 三件套,把 Django 項(xiàng)目絲滑地送上云服務(wù)器。
全程手摸手,有豐富的控制臺(tái)輸出與可復(fù)現(xiàn)的例子,新手能看懂,進(jìn)階者也能收獲最佳實(shí)踐。
1. 先弄明白:這三者分別是什么?
Gunicorn:一個(gè) WSGI HTTP 服務(wù)器,專為運(yùn)行 Python Web 應(yīng)用而生。相比 Django 自帶的
runserver,它支持多進(jìn)程、更穩(wěn)定,是生產(chǎn)環(huán)境的標(biāo)配。Nginx:高性能的反向代理和靜態(tài)文件服務(wù)器。在這里它負(fù)責(zé)接收外界請(qǐng)求,動(dòng)態(tài)內(nèi)容轉(zhuǎn)發(fā)給 Gunicorn,靜態(tài)文件直接由自己返回,極大提升效率。
Docker:將應(yīng)用及其依賴打包成容器,消除“我這能跑,你那不行”的環(huán)境差異問題。結(jié)合 Docker Compose 可以輕松編排多服務(wù)(Web + Nginx + 數(shù)據(jù)庫等)。
整體數(shù)據(jù)流:
用戶瀏覽器 -> 云服務(wù)器 80/443 端口 -> Nginx 容器 -> (動(dòng)態(tài)請(qǐng)求) -> Gunicorn 容器 -> Django 應(yīng)用
2. 預(yù)備一個(gè)示例 Django 項(xiàng)目
假設(shè)我們的項(xiàng)目名為 myblog,結(jié)構(gòu)如下:
myblog/
├── manage.py
├── myblog/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── blog/ # 一個(gè)簡單應(yīng)用
│ ├── views.py
│ └── urls.py
├── requirements.txt
└── Dockerfile快速創(chuàng)建一個(gè)最簡版本(實(shí)際操作時(shí)你可以用自己的項(xiàng)目):
django-admin startproject myblog
cd myblog
python manage.py startapp blog在 blog/views.py 中加入一個(gè)簡單視圖:
from django.http import HttpResponse
def home(request):
return HttpResponse("Hello, Django & Docker!")在 myblog/urls.py 中注冊(cè):
from django.contrib import admin
from django.urls import path
from blog.views import home
urlpatterns = [
path('admin/', admin.site.urls),
path('', home, name='home'),
]修改 settings.py,允許外部訪問(部署時(shí)務(wù)必改為具體域名或 IP):
ALLOWED_HOSTS = ['*'] # 僅測試用,生產(chǎn)環(huán)境請(qǐng)指定真實(shí)域名
STATIC_ROOT = BASE_DIR / 'staticfiles' # 收集靜態(tài)文件的目錄生成依賴文件:
pip freeze > requirements.txtrequirements.txt 內(nèi)容示例:
Django==4.2
gunicorn==21.2.03. 手寫 Dockerfile:用 Gunicorn 啟動(dòng) Django
在項(xiàng)目根目錄創(chuàng)建 Dockerfile:
# 使用官方 Python 鏡像(slim 版本更?。?FROM python:3.11-slim
# 設(shè)置環(huán)境變量,避免 Python 生成 .pyc 文件,并開啟標(biāo)準(zhǔn)輸出
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# 設(shè)置工作目錄
WORKDIR /app
# 安裝系統(tǒng)依賴(如果連接數(shù)據(jù)庫可能需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
# 復(fù)制并安裝 Python 依賴
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
# 復(fù)制項(xiàng)目代碼
COPY . .
# 收集靜態(tài)文件
RUN python manage.py collectstatic --noinput
# 暴露 8000 端口(Gunicorn 默認(rèn)監(jiān)聽)
EXPOSE 8000
# 啟動(dòng)命令:使用 Gunicorn 運(yùn)行 WSGI 應(yīng)用
CMD ["gunicorn", "myblog.wsgi:application", "--bind", "0.0.0.0:8000"]構(gòu)建鏡像,你能看到類似下面的輸出:
$ docker build -t myblog:latest .
Sending build context to Docker daemon 45.06kB
Step 1/10 : FROM python:3.11-slim
---> a1b2c3d4e5f6
Step 2/10 : ENV PYTHONDONTWRITEBYTECODE=1
---> Running in 12ab34cd56ef
---> 7890abcd1234
...
Step 10/10 : CMD ["gunicorn", "myblog.wsgi:application", "--bind", "0.0.0.0:8000"]
---> Running in ef567890abcd
---> fedc09876543
Successfully built fedc09876543
Successfully tagged myblog:latest跑起來試試:
$ docker run -d -p 8000:8000 --name myblog-test myblog:latest查看容器日志,熟悉的 Gunicorn 啟動(dòng)信息:
$ docker logs myblog-test
[2026-05-17 14:23:45 +0000] [1] [INFO] Starting gunicorn 21.2.0
[2026-05-17 14:23:45 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2026-05-17 14:23:45 +0000] [1] [INFO] Using worker: sync
[2026-05-17 14:23:45 +0000] [8] [INFO] Booting worker with pid: 8瀏覽器訪問 http://localhost:8000,就能看到 Hello, Django & Docker!。
(停止并刪除測試容器:docker rm -f myblog-test)
進(jìn)階提示:可以通過
--workers參數(shù)調(diào)整進(jìn)程數(shù),或通過環(huán)境變量WEB_CONCURRENCY動(dòng)態(tài)設(shè)置,后面會(huì)提到。
4. 引入 Nginx:更貼近生產(chǎn)環(huán)境的編排
生產(chǎn)環(huán)境中,我們不會(huì)把 Gunicorn 直接暴露給用戶,而是前面再放一個(gè) Nginx。
這里使用 Docker Compose 定義兩個(gè)服務(wù),并用共享卷處理靜態(tài)文件。
4.1 Nginx 配置文件
在項(xiàng)目根目錄創(chuàng)建 nginx/nginx.conf:
upstream myblog_app {
# web 是 docker-compose 中 Django 服務(wù)的名稱
server web:8000;
}
server {
listen 80;
server_name _; # 用真實(shí)域名時(shí)替換
# 靜態(tài)文件直接由 Nginx 提供
location /static/ {
alias /staticfiles/;
}
# 動(dòng)態(tài)請(qǐng)求轉(zhuǎn)發(fā)給 Gunicorn
location / {
proxy_pass http://myblog_app;
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;
}
}4.2 Docker Compose 編排文件
在項(xiàng)目根目錄創(chuàng)建 docker-compose.yml:
version: '3.8'
services:
web:
build: .
# 不直接暴露端口,僅內(nèi)部使用
expose:
- "8000"
volumes:
- static_volume:/app/staticfiles # 共享靜態(tài)文件卷
environment:
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY:-change-me}
- DEBUG=False
command: gunicorn myblog.wsgi:application --bind 0.0.0.0:8000 --workers 3
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static_volume:/staticfiles:ro # 只讀方式掛載靜態(tài)文件卷
depends_on:
- web
restart: unless-stopped
volumes:
static_volume:新手理解:
static_volume是一個(gè)命名卷,web 服務(wù)運(yùn)行collectstatic后靜態(tài)文件就存入其中。Nginx 掛載同一個(gè)卷,直接向用戶提供
/static/下的文件,完全不經(jīng)過 Django,速度飛快。
4.3 啟動(dòng)編排,觀察日志
$ docker-compose up -d
Creating network "myblog_default" with the default driver
Creating volume "myblog_static_volume" with default driver
Creating myblog_web_1 ... done
Creating myblog_nginx_1 ... done查看所有容器的狀態(tài):
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------------------------
myblog_nginx_1 /docker-entrypoint.sh ngin ... Up 0.0.0.0:80->80/tcp
myblog_web_1 gunicorn myblog.wsgi:applic ... Up 8000/tcp看一下日志,確認(rèn)兩個(gè)服務(wù)都正常:
$ docker-compose logs -f
# web 輸出:
web_1 | [2026-05-17 14:30:01 +0000] [1] [INFO] Starting gunicorn 21.2.0
web_1 | [2026-05-17 14:30:01 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
web_1 | [2026-05-17 14:30:01 +0000] [8] [INFO] Booting worker with pid: 8
web_1 | [2026-05-17 14:30:01 +0000] [9] [INFO] Booting worker with pid: 9
web_1 | [2026-05-17 14:30:01 +0000] [10] [INFO] Booting worker with pid: 10
# nginx 輸出:
nginx_1 | /docker-entrypoint.sh: Configuration complete; ready for start up訪問 http://localhost(Nginx 的 80 端口),同樣得到 Hello, Django & Docker!。
靜態(tài)文件測試:訪問 http://localhost/static/admin/css/base.css 能看到 Django admin 的樣式,證明 Nginx 直接返回了靜態(tài)文件。
5. 部署到真實(shí)的云服務(wù)器
假設(shè)你有一臺(tái) Ubuntu 22.04 的云服務(wù)器(阿里云、騰訊云、AWS 均可),并已通過 SSH 登錄。
5.1 服務(wù)器環(huán)境準(zhǔn)備
# 更新包索引
sudo apt update
# 安裝 Docker
curl -fsSL https://get.docker.com | sudo sh
# 啟動(dòng) Docker 并設(shè)置開機(jī)自啟
sudo systemctl enable docker --now
# 安裝 Docker Compose(獨(dú)立插件方式)
sudo apt install docker-compose-plugin -y
# 驗(yàn)證
docker --version
docker compose version控制臺(tái)輸出示例:
$ docker --version
Docker version 24.0.7, build afdd53b
$ docker compose version
Docker Compose version v2.21.05.2 上傳項(xiàng)目代碼
推薦使用 Git 管理代碼:
# 在服務(wù)器上克隆項(xiàng)目(以 GitHub 為例)
cd /opt
git clone https://github.com/yourname/myblog.git
cd myblog5.3 配置環(huán)境變量(重要)
創(chuàng)建 .env 文件存儲(chǔ)敏感信息,生產(chǎn)環(huán)境務(wù)必修改:
DJANGO_SECRET_KEY=你的超級(jí)長隨機(jī)字符串
DEBUG=False
ALLOWED_HOSTS=你的服務(wù)器IP或域名并在 docker-compose.yml 中傳遞給 web 服務(wù),同時(shí)修改 Gunicorn 命令引用環(huán)境變量:
web:
...
environment:
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DEBUG=${DEBUG:-False}
- ALLOWED_HOSTS=${ALLOWED_HOSTS}
command: >
gunicorn myblog.wsgi:application
--bind 0.0.0.0:8000
--workers ${WEB_CONCURRENCY:-3}對(duì)應(yīng)的 settings.py 中讀取這些變量:
import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'fallback-key')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')5.4 在服務(wù)器上啟動(dòng)應(yīng)用
$ docker compose up -d --build
[+] Building 23.4s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 348B 0.0s
...
[+] Running 3/3
? Network myblog_default Created 0.1s
? Container myblog-web-1 Started 1.2s
? Container myblog-nginx-1 Started 1.5s檢查運(yùn)行狀態(tài):
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
myblog-nginx-1 "/docker-entrypoint.…" nginx running 0.0.0.0:80->80/tcp
myblog-web-1 "gunicorn myblog.wsgi…" web running 8000/tcp現(xiàn)在,在 云服務(wù)器的安全組 中開放 80 端口,用瀏覽器訪問服務(wù)器公網(wǎng) IP,就能看到你的 Django 應(yīng)用了!
6. 進(jìn)階實(shí)戰(zhàn):加入 PostgreSQL 數(shù)據(jù)庫
SQLite 不適合生產(chǎn),我們?cè)偌右粋€(gè)數(shù)據(jù)庫服務(wù),完整演示多容器編排。
修改 docker-compose.yml,增加 db 服務(wù):
services:
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_DB=${DB_NAME:-myblog}
- POSTGRES_USER=${DB_USER:-mybloguser}
- POSTGRES_PASSWORD=${DB_PASSWORD:-securepassword}
restart: unless-stopped
web:
build: .
expose:
- "8000"
volumes:
- static_volume:/app/staticfiles
environment:
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DEBUG=False
- DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
depends_on:
- db
command: >
gunicorn myblog.wsgi:application
--bind 0.0.0.0:8000
--workers 3
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static_volume:/staticfiles:ro
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
static_volume:在 .env 中增加:
DB_NAME=myblog
DB_USER=mybloguser
DB_PASSWORD=你的復(fù)雜密碼在 Django 的 settings.py 使用 dj-database-url 解析數(shù)據(jù)庫連接(需加入 requirements.txt):
import dj_database_url
DATABASES = {
'default': dj_database_url.config(
default=os.environ.get('DATABASE_URL', 'sqlite:///db.sqlite3')
)
}重新構(gòu)建并啟動(dòng):
$ docker compose up -d --build
Creating myblog_db_1 ... done
Creating myblog_web_1 ... done
Creating myblog_nginx_1 ... done查看數(shù)據(jù)庫容器日志:
$ docker compose logs db
db_1 | 2026-05-17 14:45:00.123 UTC [1] LOG: database system is ready to accept connections執(zhí)行數(shù)據(jù)庫遷移:
$ docker compose exec web python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
...完美!現(xiàn)在你的 Django 應(yīng)用已經(jīng)連接上了生產(chǎn)級(jí) PostgreSQL。
7. 更進(jìn)一步:HTTPS、CI/CD 等最佳實(shí)踐(簡覽)
7.1 開啟 HTTPS
可以在 Nginx 容器內(nèi)配置 SSL,最簡單的方法是使用 certbot 及其 Nginx 插件,但容器化環(huán)境下推薦使用 Certbot 官方鏡像 或 Traefik。這里給出一個(gè)手動(dòng)整合 Certbot 的思路:
先以 HTTP 啟動(dòng),通過
certbot獲取證書,映射到宿主機(jī)的證書目錄。修改 Nginx 配置監(jiān)聽 443,證書路徑指向掛載的文件。
使用
docker compose exec nginx nginx -s reload熱重載。
關(guān)鍵 Nginx SSL 配置片段:
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
...
}7.2 使用 CI/CD 自動(dòng)部署
例如 GitHub Actions 工作流,在推送代碼后自動(dòng)構(gòu)建鏡像并部署到服務(wù)器:
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myblog
git pull
docker compose up -d --build7.3 性能與監(jiān)控
Worker 數(shù)量:建議
(2 * CPU核心數(shù)) + 1,可通過WEB_CONCURRENCY環(huán)境變量調(diào)整。日志:
docker compose logs -f實(shí)時(shí)查看,生產(chǎn)環(huán)境建議接入 ELK 或 Loki。健康檢查:在
docker-compose.yml中為 web 服務(wù)添加healthcheck,配合depends_on條件使用condition: service_healthy。
8. 常見問題快速排查
9. 總結(jié)
通過這篇長文,我們從零開始,經(jīng)歷了:
創(chuàng)建一個(gè)簡單的 Django 項(xiàng)目
用 Dockerfile 將其容器化,并用 Gunicorn 啟動(dòng)
用 Docker Compose 編排 Nginx + Gunicorn,處理靜態(tài)文件
部署到云服務(wù)器并連接 PostgreSQL
探索了 HTTPS、CI/CD 等進(jìn)階方向
Gunicorn + Nginx + Docker 這套組合已經(jīng)成為 Django 生產(chǎn)部署的事實(shí)標(biāo)準(zhǔn)。它隔離環(huán)境、簡化運(yùn)維、易于擴(kuò)展,無論是個(gè)人項(xiàng)目還是企業(yè)級(jí)應(yīng)用都能游刃有余。
希望這篇文章能幫你跨過從開發(fā)到上線的鴻溝,享受流暢的部署體驗(yàn)。
想了解更多,還可以去公眾號(hào)、今日頭條搜索「IT策士」,一起升級(jí) IT 思維 !