Django1:部署——从本地启动到服务器运行
本系列目录
本系列以 Django 5.2 LTS 和个人博客为例。Django1—Django8 是文章序号。
- Django1:部署——从本地启动到服务器运行
- Django2:配置——看懂项目结构与请求流程
- Django3:数据库——模型设计、关系与迁移
- Django4:ORM——增删改查、事务与查询优化
- Django5:页面——视图、模板、搜索与分页
- Django6:使用——表单、文章管理与 Admin
- Django7:权限——登录、会话与访问控制
- 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/,进入它。不要再运行 startproject 和 startapp。
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:配置 会解释这些配置如何影响每一次请求。