本系列目录

本系列以 Django 5.2 LTS 和个人博客为例。Django1—Django8 是文章序号。

  1. Django1:部署——从本地启动到服务器运行
  2. Django2:配置——看懂项目结构与请求流程
  3. Django3:数据库——模型设计、关系与迁移
  4. Django4:ORM——增删改查、事务与查询优化
  5. Django5:页面——视图、模板、搜索与分页
  6. Django6:使用——表单、文章管理与 Admin
  7. Django7:权限——登录、会话与访问控制
  8. Django8:维护——测试、日志、备份与升级

下载全部教程与可运行示例(ZIP)。解压后进入 django-series/example/,按第一篇运行。

查看完整项目源码 · 查看示例验证范围

假设我们要搭建一个个人博客:游客看文章,作者登录后写文章,管理员维护分类和账号。第一步是让这个系统运行起来。本篇提供两条入口:从零创建一个最小项目,或者直接运行本系列的完整示例。

全系列以 Django 5.2 LTS 为基线,建议使用 Python 3.12 或 3.13。它们属于 Django 5.2 支持的 Python 版本。安装时限制在 5.2.x,让后续代码与同一版本文档对应。Django 5.2 版本说明

1. 先认识部署中的几个角色

本地开发时,浏览器可以直接连接 Django 的开发服务器。服务器部署时,本例采用以下结构:

浏览器 → 外层 Nginx(HTTPS)→ 内层 Nginx → Gunicorn → Django → PostgreSQL
                                 └──── /static/ 静态文件
组件 在本例中的职责
Django 路由匹配、业务处理、数据库访问、模板渲染
Gunicorn 加载 Django 的 WSGI 应用,接收代理转来的请求
Nginx 处理反向代理、静态文件;外层负责 HTTPS
PostgreSQL 保存用户、文章、分类、标签和会话
Docker Compose 声明容器、环境变量、网络关系和持久化卷

python manage.py runserver 适合本地调试。正式服务需要使用面向部署的应用服务器,并检查生产配置。Django 部署说明

2. 路线 A:从零创建最小项目

前提是电脑已安装 Python。macOS/Linux 的示例命令如下;若 python3 不是 3.12 或 3.13,先选择合适的解释器。

python3 --version
mkdir django-blog
cd django-blog
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install 'Django>=5.2,<5.3'
python -m django --version
python -m django startproject config .
python manage.py startapp blog
python manage.py migrate
python manage.py runserver 127.0.0.1:8000

Windows PowerShell 创建环境时可以使用 py -3.12 -m venv .venv,随后运行 .\.venv\Scripts\Activate.ps1。若当前策略不允许激活脚本,可以直接使用 .\.venv\Scripts\python.exe 执行后面的 Python 命令,无需改变系统策略。

startproject config . 最后的点表示“把项目创建在当前目录”。没有这个点,Django 会再建立一层目录。虚拟环境让本项目的依赖与其他项目分开;python -m pip 则明确使用当前 Python 对应的 pip。

打开 本地首页,此时应看到 Django 欢迎页。终端保持运行;按 Ctrl+C 停止服务。这一阶段生成的是框架骨架,博客功能会在后续文章逐步加入。官方入门教程

3. 路线 B:直接运行完整博客

如果希望先看到成品,下载本页开头的配套 ZIP,解压后将 django-series/example/ 整个目录复制成自己的 django-blog/,进入它。不要再运行 startprojectstartapp

python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver 127.0.0.1:8000

createsuperuser 会交互式要求填写用户名、邮箱和密码,输入密码时终端不显示字符属于正常现象。演示账号的密码由你自己设置。

地址 预期结果
/ 显示“最新文章”,第一次通常为空
/healthz/ 返回 {"status": "ok"}
/admin/ 显示后台登录页
/accounts/login/ 显示网站作者登录页
/my/posts/ 未登录时跳转到登录页

第一次使用:登录后台,创建“技术”分类和“Django”标签;然后到前台“写文章”,输入标题、英文网址标识和正文,把状态选为“已发布”并保存。回到首页就能看见这篇文章。完整使用说明见 Django6

4. 配置文件与密钥分开管理

服务器使用 .env.example 作为模板:

DJANGO_DEBUG=0
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost,blog.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://blog.example.com
DJANGO_HTTPS=0
DJANGO_TRUST_PROXY=0
DJANGO_HSTS_SECONDS=0
POSTGRES_DB=blog
POSTGRES_USER=blog_app
POSTGRES_PASSWORD=replace-with-a-random-database-password
POSTGRES_ADMIN_PASSWORD=replace-with-a-different-random-admin-password

复制后编辑 .env

cp .env.example .env
chmod 600 .env
python -c 'import secrets; print(secrets.token_urlsafe(64))'

最后一条命令生成随机值。分别生成 Django 密钥、数据库应用密码、数据库管理员密码,填入对应变量;不要原样使用 replace-with-...。将 blog.example.com 替换为实际域名。.env 已列入 .gitignore.dockerignore

本项目由 Compose 读取 .env 并显式传入所需变量。直接运行 python manage.py ... 时不会自动读取 .env 本地 SQLite 路线默认不需要它;要在本机启用其他配置,使用终端环境变量,详见第二篇。

5. 构建应用镜像

以下是完整 Dockerfile

FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
    && useradd --create-home --uid 10001 app
COPY --chown=app:app . .
RUN mkdir -p /app/staticfiles && chown app:app /app/staticfiles
USER app
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "--error-logfile", "-"]

应用运行用户为 app,静态文件目录归它所有。Gunicorn 监听容器内部的 8000 端口,并将访问日志与错误日志写到标准输出。两个 worker 是本例的起点,后续要根据内存和请求负载调整。WSGI 入口的工作方式见 Django 与 Gunicorn

requirements.txt 使用版本范围,便于学习时安装对应系列的更新。正式发布前,应在目标构建环境生成并审核依赖锁定文件,固定镜像标签或摘要,并保留上一版镜像,避免每次重建都获得不同依赖。

6. 编排数据库、应用与代理

服务器或本机需要先安装 Docker Engine/Desktop 和 Compose 插件,可按 Docker 官方安装说明 完成。在 manage.py 所在目录使用以下 compose.yaml

name: django-blog-tutorial

services:
  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env}
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD:?set POSTGRES_ADMIN_PASSWORD in .env}
      POSTGRES_APP_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
      POSTGRES_APP_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./deploy/init-db.sh:/docker-entrypoint-initdb.d/10-app.sh:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 5s
      retries: 12

  web:
    build: .
    restart: unless-stopped
    environment:
      DJANGO_DEBUG: "0"
      DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?set DJANGO_SECRET_KEY in .env}
      DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-127.0.0.1,localhost}
      DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS:-}
      DJANGO_HTTPS: ${DJANGO_HTTPS:-0}
      DJANGO_TRUST_PROXY: ${DJANGO_TRUST_PROXY:-0}
      DJANGO_HSTS_SECONDS: ${DJANGO_HSTS_SECONDS:-0}
      DB_ENGINE: postgres
      POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env}
      POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_HOST: db
      POSTGRES_PORT: "5432"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - static_data:/app/staticfiles

  nginx:
    image: nginx:stable
    restart: unless-stopped
    ports:
      - "127.0.0.1:8088:80"
    volumes:
      - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - static_data:/static:ro
    depends_on:
      - web

volumes:
  postgres_data:
  static_data:

这里有四个容易忽略的细节。第一,数据库没有向宿主机公开 5432 端口。第二,网站只绑定 127.0.0.1:8088,方便本机检查与外层代理接入。第三,数据库和静态文件分别放在持久化卷里。第四,web 等待数据库健康检查通过后启动;仅声明启动顺序不能保证数据库已经可以接受连接。Compose 启动顺序

数据库镜像的初始化管理员是 postgres,Django 使用的是普通角色 blog_app。初始化脚本 deploy/init-db.sh 在首次创建空数据库卷时执行:

#!/bin/sh
set -eu
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
  --set=app_user="$POSTGRES_APP_USER" \
  --set=app_password="$POSTGRES_APP_PASSWORD" \
  --set=db_name="$POSTGRES_DB" <<'SQL'
CREATE ROLE :"app_user" LOGIN PASSWORD :'app_password';
GRANT CONNECT ON DATABASE :"db_name" TO :"app_user";
GRANT USAGE, CREATE ON SCHEMA public TO :"app_user";
SQL

应用角色只能登录并在本项目数据库的 public schema 创建对象,不具有 PostgreSQL 超级用户权限。对于更严格的部署,可以再分离迁移角色和日常读写角色。初始化脚本只在空数据目录执行;修改 .env 里的密码不会自动修改现有数据库角色的密码。PostgreSQL 官方容器说明

7. 首次启动的顺序

docker compose build web
docker compose up -d --wait db
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py collectstatic --noinput
docker compose run --rm web python manage.py createsuperuser
docker compose up -d web nginx
docker compose ps
curl http://127.0.0.1:8088/healthz/

此处 --wait 需要较新的 Compose v2。迁移和收集静态文件是显式的发布步骤,执行成功后再启动服务。第一次打开 /admin/ 时,登录页面应有正常样式。

内层 Nginx 的完整配置位于 deploy/nginx.conf

# 内层仅监听容器网络;宿主机只发布 127.0.0.1:8088。
# 接入 HTTPS 后,只有可信外层代理能访问它,并覆盖 X-Forwarded-Proto。
map $http_x_forwarded_proto $original_scheme {
    default $scheme;
    https https;
}

server {
    listen 80;
    server_name _;
    client_max_body_size 2m;

    location /static/ {
        alias /static/;
        expires 1h;
    }

    location / {
        proxy_pass http://web:8000;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $original_scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

/static/ 从共享卷读取文件;其他请求转给 web:8000。Compose 内部 web 是服务名,所以容器间可以通过这个名称访问。collectstatic 汇集 CSS、JavaScript、Admin 样式等资源,不负责生成业务页面。静态文件部署说明

8. 接入域名和 HTTPS

前面的 HTTP 地址用于本机验收。上线前,将域名 DNS 指向服务器,并在可信外层代理上完成证书签发和续期配置。如果服务器上已有 Nginx Proxy Manager、Traefik 或其他统一入口,应给博客新增一个站点路由。

下面是外层 Nginx 直接运行在宿主机、且证书已经存在时的示例。将它保存到该 Nginx 的站点配置目录,替换域名和证书路径;证书可通过现有证书管理工具或 Certbot 官方指引 获取。

server {
    listen 80;
    server_name blog.example.com;
    return 301 https://blog.example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name blog.example.com;
    ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8088;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

外层代理必须覆盖客户端提交的 X-Forwarded-Proto。内层端口只允许可信代理访问,Django 才能依赖这个头判断原始请求是否使用 HTTPS。若外层代理也在容器里,127.0.0.1 指向那个容器自身,需要让外层代理加入明确的共享网络并转发到内层 Nginx 服务名;不能直接照抄宿主机地址。Nginx 代理头配置

确认 HTTPS 链路正常后,将 .env 改为:

DJANGO_ALLOWED_HOSTS=blog.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://blog.example.com
DJANGO_HTTPS=1
DJANGO_TRUST_PROXY=1
DJANGO_HSTS_SECONDS=3600

先用较短 HSTS 时间验证证书续期和所有页面,再按实际策略增加。使用宿主机 Nginx 时,检查并重载配置,然后重建应用容器以加载新环境变量:

sudo nginx -t
sudo systemctl reload nginx
docker compose up -d --force-recreate web nginx
docker compose run --rm web python manage.py check --deploy
curl -I https://blog.example.com/

开启 HTTPS 重定向后,内层 HTTP 请求可能返回跳转,这时应从最终 HTTPS 域名验收。check --deploy 的提示要逐项理解;本例未启用 HSTS 的子域覆盖和预加载,因为它们需要对整个域名体系作出承诺。生产部署检查表

9. 常见问题与验收

现象 优先检查
No module named django 当前 Python 是否来自 .venv,依赖是否安装到同一个解释器
DisallowedHost 最终访问的域名/IP 是否在 DJANGO_ALLOWED_HOSTS
no such table / relation does not exist 是否对当前连接的数据库执行过 migrate
Admin 页面没有样式 collectstatic 是否成功,Nginx 是否挂载同一个静态卷
502 docker compose logs --tail 100 web nginx,确认 Gunicorn 已启动
HTTPS 无限重定向 每层代理是否正确传递协议,Django 是否只信任可信代理
修改 .env 没生效 容器环境需重建;restart 不会重新注入环境变量
修改数据库密码后无法登录 已有卷不会重跑初始化脚本,需要实际修改角色密码

完成本篇时,你应能启动本地博客,解释每个部署组件的用途,并知道服务器部署的先后顺序。下一篇 Django2:配置 会解释这些配置如何影响每一次请求。


下一篇:Django2:配置——看懂项目结构与请求流程