第 16 章:部署

本地 Django 开发环境追求的易用性与生产环境所需的安全性和性能之间存在根本性的矛盾。Django 旨在让 Web 开发者的生活更轻松,因此在首次运行 startproject 命令时,它默认使用本地配置。我们已经看到了这一点:使用 SQLite 作为本地文件型数据库,内置的 runserver 命令在浏览器中启动本地 Web 服务器,以及 settings.py 文件中的各种默认配置,包括 DEBUG 设置为 True 和自动生成的 SECRET_KEY

在生产环境中,情况则不同。所有在开发环境中优化为易用的配置,都需要转而关注安全性、性能和可扩展性。

将 Django 网站部署到生产环境需要多个步骤——事实上步骤很多,以至于 Django 文档甚至提供了一个部署清单。它是一个非常有用的工具,但不幸的是,仅此还不够。其他因素包括各种托管选项、环境变量、数据库配置、静态文件处理等。

在本章中,我们将创建一个部署清单,并使用 Heroku 部署 Newspaper 项目。所涵盖的技术几乎适用于任何需要准备好投入生产的 Django 网站,无论托管平台是什么。

托管选项

如果你问五个 Django 开发者最好的托管选项是什么,你可能会得到五个不同的答案。每个人都有自己的偏好,取决于他们的经验和项目需求。不过,我们可以将托管选项分为三大类:

  1. 专用服务器(Dedicated Server):位于数据中心、完全属于你的物理服务器。通常只有大公司采用这种方法,因为配置和维护需要大量技术专业知识。
  2. 虚拟私有服务器(VPS):一台服务器可以划分为多个使用相同硬件的虚拟机,这比专用服务器便宜得多。这种方法也意味着你无需担心硬件的维护。
  3. 平台即服务(PaaS):一种预先配置和维护的托管型 VPS 解决方案,是部署和扩展网站最快的方式。它通常还附带托管的数据库。缺点在于开发者无法像 VPS 或专用服务器那样进行高度定制。在规模扩大时,PaaS 可能会变得相当昂贵。

这里的选择都涉及权衡利弊。许多 Django 开发者和小公司乐于使用 PaaS 来抽象化将代码投入生产环境所固有的许多困难。流行的 PaaS 选项包括 Heroku、Fly 和 Render 等。对于 VPS,Digital Ocean 对独立开发者或小团队有最简洁的界面,而对于企业级应用,通常的选择是 AWS、Google 或 Microsoft。

对于我们的 Newspaper 网站,我们将使用 PaaS,具体来说是 Heroku,因为它成熟、使用广泛,而且部署流程相对简单。不过,除了最后创建 Heroku 特定的 Procfile 文件外,本章概述的步骤也适用于任何 PaaS 提供商。

Web 服务器与 WSGI/ASGI 服务器

Django 通过 runserver 提供的本地服务器承担了多个在生产环境中必须以不同方式处理的工作。首先,它充当 Web 服务器,即位于 Django 应用之前处理 HTTP 请求和响应的软件。它还管理静态文件请求。在平台即服务出现之前,Web 开发者需要自己设置 Web 服务器——通常是 NginxApache。如今,PaaS 知道你正在部署一个网站,会自动捆绑一个 Web 服务器(通常是 Nginx),这样我们就不必自己安装或管理了。

runserver 为我们提供的另一个角色是充当应用服务器,帮助 Django 生成动态内容。当请求进来时,runserver 驱动该请求经过 URL、视图、模型、数据库和模板,然后生成 HTTP 响应。换句话说,runserver 不仅充当 Web 服务器,还充当应用服务器。

应用服务器通常被称为”WSGI 服务器”,因为它们使用 WSGI 将 Python Web 应用连接到服务器。在 Web 开发的早期,Web 框架不能隐式地与各种 Web 服务器良好配合,需要进行大量定制。对于 Python Web 框架,这导致了 2003 年 Web 服务器网关接口(WSGI)的创建。WSGI 既不是服务器也不是框架,而是一套规则,标准化了 Web 服务器应如何连接到任何 Python Web 应用。通过抽象掉这个麻烦,它为更新的 Python Web 框架——比如 2005 年首次发布的 Django——打开了大门,使它们可以在任何 Web 服务器上运行,而无需担心这个步骤。生产环境中常见的 WSGI 服务器包括 Gunicorn、uWSGI 和 Daphne。

传统上,Python 是一种同步编程语言:代码按顺序执行,意味着每段代码必须完成后另一段代码才能开始。因此,复杂的任务可能需要一些时间。从 2012 年的 Python 3.3 开始,通过 [asyncio](https://docs.python.org/3/library/asyncio.html) 模块为 Python 添加了异步编程支持。同步处理是按特定顺序依次进行的,而异步处理则是并行进行的。不依赖于其他任务的任务可以卸载并在主操作的同时执行,完成后返回结果。

Django 一直在逐步增加对异步代码的支持,通过每个主要发布版本将更多层面转换为异步。Django 支持流行的 ASGI(异步服务器网关接口),它于 2015 年作为 WSGI 的演进版本创建。ASGI 支持同步和异步代码,这对于实时应用至关重要。自从 Django 3.0 以来,ASGI 支持已经被包含在内。该配置位于 django_project/asgi.py 文件中。

Django 整个技术栈的完全异步支持仍在开发中,但随着每个主要版本的发布越来越接近。鉴于本书面向初学者,重要的是认识到异步的发展,而不是深入探讨。虽然从技术角度来看令人兴奋,但它们也较难理解,并且主要适用于需要”实时”功能的网站,例如实时通知、聊天应用、实时数据更新和交互式仪表板。

部署清单

一开始就看到完整的部署清单可能会让人不知所措,但它是本章的有用指南。以下是我们将要涵盖的部署清单

  • 配置静态文件并安装 WhiteNoise
  • 使用 environs 添加环境变量
  • 创建 .env 文件并更新 .gitignore 文件
  • 更新 DEBUG[ALLOWED_HOSTS](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-ALLOWED_HOSTS)SECRET_KEYCSRF_TRUSTED_ORIGINS
  • 更新 DATABASES 以在生产环境中运行 PostgreSQL 并安装 psycopg
  • 安装 Gunicorn 作为生产 WSGI 服务器
  • 创建 Procfile
  • 更新 requirements.txt 文件
  • 创建新的 Heroku 项目,推送代码,启动 dyno Web 进程

我们可以切换更多生产设置,但这个列表涵盖了最关键的安全和性能问题。

静态文件

静态文件是网站使用的图片、JavaScript 和 CSS。我们在第六章的 Blog 项目中处理过它们,当时我们添加了自定义 CSS。在本地使用时,只要 settings.py 中的 DEBUG 设置为 True,这些文件就会由 runserver 命令自动提供。

Django 会自动在每个应用的名为”static”的文件夹中查找静态文件,但一个常见的技术是将所有静态文件放在项目级别的名为”static”的文件夹中。我们将这样做。按 Control+c 退出本地服务器,在与 manage.py 文件相同的文件夹中创建一个新的静态目录。在命令行中在其内部添加 cssjsimg 文件夹。

(.venv) $ mkdir static
(.venv) $ mkdir static/css
(.venv) $ mkdir static/js
(.venv) $ mkdir static/img

Git 的一个特性是不会跟踪空文件夹。如果文件夹内没有文件,Git 默认会忽略它。一种解决方案——我们将采用它——是用文本编辑器向三个子文件夹中添加一个 .keep 文件:

  • static/css/.keep
  • static/js/.keep
  • static/img/.keep

在本地使用时,静态文件只需要两个设置:STATIC_URL(提供静态文件的基础 URL)和 STATICFILES_DIRS(定义内置的 staticfiles 应用在 app/static 文件夹之外搜索静态文件的额外位置)。

# django_project/settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]     # 新增

我们的本地 Django 服务器不适合在生产环境中托管静态文件。最佳实践是将所有静态文件打包到一个目录中,然后由生产 Web 服务器(而不是 Django 服务器)来提供它们。Django 为此提供了一个管理命令 collectstatic:它将所有静态文件复制到单个位置以便部署。我们需要配置的是设置 STATIC_ROOT 来定义编译后的静态文件的位置。按照惯例,我们将创建一个名为 staticfiles 的新项目级目录。

# django_project/settings.py
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"       # 新增

现在运行命令 python manage.py collectstatic 将所有静态文件编译到 staticfiles 文件夹中。

(.venv) $ python manage.py collectstatic

目前唯一的静态文件包含在内置的 admin 应用中,因此新的 staticfiles 目录中会出现 admin 的相关内容。将来添加更多静态文件时,它们也会被编译到此目录中。

# staticfiles/
└── admin
    ├── css
    ├── img
    └── js

我们需要使用 {% load static %} 模板标签来在模板中显示静态文件。现在将其添加到 base.html 文件的顶部。

<!-- templates/base.html -->
{% load static %}
<!DOCTYPE html>
...

如果我们使用专用服务器或 VPS 进行部署,就需要自己为 Web 服务器(通常是 NginxApache)编写提供静态文件的代码。但由于我们使用的是 PaaS Heroku,我们可以利用流行的 WhiteNoise 第三方包,它经过优化,可以从 Django 高效地提供静态文件。它支持额外的压缩和不可变文件存储,并设置适当的 HTTP 缓存头。简而言之,它使我们的部署过程更加简单。

使用 pip 安装最新版本的 WhiteNoise

(.venv) $ python -m pip install whitenoise==6.7.0

然后,在 django_project/settings.py 文件中,进行三项更改:

  • 将 whitenoise 添加到 INSTALLED_APPS 中,位于内置的 staticfiles 应用之上
  • MIDDLEWARE 中,在第三行添加新的 WhiteNoiseMiddleware
  • [STORAGES](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STORAGES) 改为使用 WhiteNoise
# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "whitenoise.runserver_nostatic",         # 新增
    "django.contrib.staticfiles",
    # 第三方
    "crispy_forms",
    "crispy_bootstrap5",
    # 本地
    "accounts",
    "pages",
    "articles",
]

MIDDLEWARE = 
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.[middleware/).SessionMiddleware",
    "whitenoise.middleware/).WhiteNoiseMiddleware",  # 新增
    "django.middleware/).common.CommonMiddleware",
    "django.middleware/).csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware/).AuthenticationMiddleware",
    "django.contrib.messages.middleware/).MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
...
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES) = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",  # 新增
    },
}

[STORAGES](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STORAGES) 是 Django 4.2+ 中的一个新设置,用于定义文件的存储方式。它在 settings.py 中是隐式设置的,但我们要更改 staticfiles 部分以使用 WhiteNoise 压缩。

再次运行 collectstatic 命令。提示会警告覆盖现有文件,但这是有意的:我们现在要使用 WhiteNoise 编译它们。输入 yes 并按回车继续:

(.venv) $ python manage.py collectstatic

就这样!我们已经配置好了静态文件,使其在生产环境中编译到一个位置,将静态模板标签添加到 base.html 模板,并安装了 WhiteNoise 以高效地提供它们。

中间件

添加 WhiteNoise 是我们第一次更新 Django 中间件,它是一个钩子框架,位于 Django 的请求/响应处理流程中。这是一种添加功能的方式,例如认证、安全、会话等。在 HTTP 请求阶段,中间件MIDDLEWARE 中定义的顺序从上到下应用。也就是说,SecurityMiddleware 最先应用,然后是 SessionMiddleware,依此类推。

# django_project/settings.py
MIDDLEWARE = 
    "django.middleware.security.SecurityMiddleware",         │
    "django.contrib.sessions.[middleware.SessionMiddleware",   │
    "whitenoise.middleware/).WhiteNoiseMiddleware",             │  # 新增
    "django.middleware/).common.CommonMiddleware",              │
    "django.middleware/).csrf.CsrfViewMiddleware",              │
    "django.contrib.auth.middleware/).AuthenticationMiddleware",│
    "django.contrib.messages.middleware/).MessageMiddleware",   │
    "django.middleware.clickjacking.XFrameOptionsMiddleware", │
    v
]

在 HTTP 响应阶段(视图被调用后),中间件按相反顺序从下到上应用,从 XFrameOptionsMiddleware 开始,然后是 MessageMiddleware,依此类推。描述中间件的传统方式就像洋葱一样,每个中间件类都是包裹视图的一个”层”。

深入探讨中间件是一个超出本书范围的高级主题。然而,从概念上理解 Django 架构中各个部分如何协同工作是很重要的。

环境变量

现实世界中的 Django 项目至少需要两个环境(本地和生产环境),但如果涉及多个测试服务器,通常会有更多环境。在同一项目中的不同环境之间切换有两种方式:环境变量和多个设置文件。目前最流行的方法是使用环境变量,我们将采用此方式。环境变量是其值在当前程序外部设置、可以在运行时加载的变量。我们可以安全地存储这些变量,并根据需要加载到 Django 项目中。

有多种使用环境变量的方法,但对于本书,我们将使用 [environs](https://github.com/sloria/environs),一个受欢迎的第三方包,它附带额外的 Django 特定功能。使用 pip 添加 environs,并包含双引号 "" 以安装有用的 Django 扩展。

(.venv) $ python -m pip install "environs[django]"==11.0.0

然后,在 django_project/settings.py 文件顶部添加三行新代码。

# django_project/settings.py
from pathlib import Path
from environs) import Env                    # 新增

env = Env()                                  # 新增
env.read_env()                               # 新增

接下来,在项目根目录创建一个新文件 .env,包含我们的环境变量。我们已经知道任何以句点开头的文件或目录都会被当作隐藏文件,默认不会在目录列表中显示。该文件仍然存在,需要添加到 .gitignore 文件中,以避免被纳入 Git 源代码控制。

# .gitignore
.venv/
__pycache__/
db.sqlite3
.env                                          # 新增!

DEBUG 和 ALLOWED_HOSTS

我们将使用环境变量配置的第一个设置是 DEBUG。默认情况下,DEBUG 设置为 True,这对本地开发很有帮助,但如果部署到生产环境将是一个重大的安全问题。例如,如果你用 python manage.py runserver 启动本地服务器,然后导航到一个不存在的页面,比如 http://127.0.0.1:8000/debug,你会看到以下内容:

!调试页面

此页面列出了所有尝试过的 URL 和加载的应用,对于任何试图入侵你网站的黑客来说,这简直就是一张藏宝图。你甚至会在错误页面底部看到,如果 DEBUG=False,Django 会显示一个标准的 404 页面。这正是我们想要的!第一步是在 settings.py 中将 DEBUG 改为 False

# django_project/settings.py
DEBUG = False

刷新网页 http://127.0.0.1:8000/debug,你会看到一个错误:网站根本无法加载。在命令行中,Django 通过 [CommandError](https://docs.djangoproject.com/en/5.0/howto/custom-management-commands/#django.core.management.CommandError)r](https://docs.djangoproject.com/en/5.0/howto/custom-management-commands/#django.core.management.[CommandError](https://docs.djangoproject.com/en/5.0/howto/custom-management-commands/#django.core.management.CommandError) 向我们提供了解释,该错误是针对严重问题抛出的。

(.venv) $ python manage.py runserver
...
CommandErrorCommandError)r](https://docs.djangoproject.com/en/5.0/howto/custom-management-commands/#django.core.management.)CommandError): You must set settings.ALLOWED_HOSTS) if DEBUG is False.

在这种情况下,Django 告诉我们,如果没有设置 [ALLOWED_HOSTS](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-ALLOWED_HOSTS),就不能将 DEBUG 设置为 False。那么什么是 [ALLOWED_HOSTS](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-ALLOWED_HOSTS)?它是一个字符串列表,代表我们的 Django 网站可以提供的主机/域名。默认情况下,[ALLOWED_HOSTS](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-ALLOWED_HOSTS) 设置为接受所有主机,这是不安全的!我们必须更新它,以接受本地端口(localhost127.0.0.1)以及 Heroku 部署的 .herokuapp.com。我们可以将所有三个路由添加到配置中。

# django_project/settings.py
ALLOWED_HOSTS = [".herokuapp.com", "localhost", "127.0.0.1"]  # 新增

现在我们已经设置了 ALLOWED_HOSTS,再次尝试 runserver 命令。

!未找到页面

这是生产环境中我们希望显示的通用 Django 404 页面。它不会向潜在的黑客泄露任何信息。

手动为开发和生产环境设置配置并不理想。首先,这非常麻烦且容易出错。其次,如果将应该保密的生信息放入 settings.py 并错误地执行了 Git 提交,那也是不安全的。

这就是环境变量的用武之地。要向项目中添加任何环境变量,我们首先将其添加到 .env 文件中,然后更新 django_project/settings.py

.env 文件中,创建一个名为 DEBUG 的新环境变量并将其值设置为 True

# .env
DEBUG=True

然后在 django_project/settings.py 中,将 DEBUG 设置改为从 .env 文件中读取变量 "DEBUG"

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

env.bool 的语法表示从 .env 文件加载一个布尔类型的环境变量(值为 true 或 false),名称为 “DEBUG”。如果找不到环境变量,则使用这里设置为 False 的默认值。最佳实践是默认使用生产设置,因为它们更安全,如果我们的代码出现问题,不会默认暴露所有秘密。

SECRET_KEY 和 CSRF_TRUSTED_ORIGINS

SECRET_KEY 是一个每次运行 startproject 时生成的随机 50 字符字符串。这个字符串为我们的 Django 项目提供加密保护。在设置文件中,你会看到以 django-insecure 开头的当前值。以下是我项目中 django_project/settings.pySECRET_KEY 的值。你的会不同。

# django_project/settings.py
SECRET_KEY = "django-insecure-3$k(g9eheqqbzr@#&tt)r6%ab-g1=!j@2c^y7*sl6+ltzys05!"

下面是在 .env 文件中的表示(不加双引号):

# .env
DEBUG=True
SECRET_KEY=django-insecure-3$k(g9eheqqbzr@#&tt)r6%ab-g1=!j@2c^y7*sl6+ltzys05!  # 新增

更新 django_project/settings.py,使 SECRET_KEY 指向这个新的环境变量。它是一个字符串,所以语法是 env.str

# django_project/settings.py
SECRET_KEY = env.str("SECRET_KEY")

现在 SECRET_KEY 已经从设置文件中移出,安全了,对吧?实际上不是!因为之前我们做了包含该值的 Git 提交,无论我们做什么,它都存储在我们的 Git 历史中。解决方案是创建一个新的 SECRET_KEY 并添加到 .env 文件中。生成新密钥的一种方法是在命令行上运行 python -c 'import [secrets](https://docs.python.org/3/library/secrets.html).html); print([secrets](https://docs.python.org/3/library/secrets.html).html).token_urlsafe())' 来调用 Python 内置的 [secrets](https://docs.python.org/3/library/secrets.html).html) 模块。

将这个新值复制粘贴到 .env 文件中。

# .env
DEBUG=True
SECRET_KEY=imDnfLXy-8Y-YozfJmP2Rw_81YA_qx1XKl5FeY0mXyY

现在使用 python manage.py runserver 重启本地服务器并刷新你的网站。它将使用从 .env 文件加载的新 SECRET_KEY 正常工作,但由于 .env.gitignore 文件中,Git 不会跟踪它。

我们的 Newspaper 项目要求在生产网站上登录管理后台以创建、读取、更新或删除文章。这意味着必须正确配置 CSRF_TRUSTED_ORIGINS,因为它是用于 POST 等不安全 HTTP 请求的可信来源列表。将其添加到 settings.py 底部,设置为匹配 Heroku 上的生产 URL https://*.herokuapp.com。我们将在本章末尾将 [ALLOWED_HOSTS](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-ALLOWED_HOSTS)CSRF_TRUSTED_ORIGINS 都更新为匹配我们的生产 URL。

# django_project/settings.py
CSRF_TRUSTED_ORIGINS = ["https://*.herokuapp.com"]  # 新增

DATABASES

我们希望本地使用 SQLite,但生产环境使用 PostgreSQL。目前,我们的 DATABASES 设置文件只列出了 SQLite。ENGINE 指定了要使用的数据库类型,NAME 指向其位置。

# django_project/settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

大多数 PaaS 会自动设置一个 DATABASE_URL 环境变量,该变量受十二要素应用方法的启发,包含连接到数据库所需的所有参数。对于 PostgreSQL 来说,原始的格式看起来像这样:

postgres://USER:PASSWORD@HOST:PORT/NAME

换句话说,使用 postgres,这里包含 USER、PASSWORD、HOST、PORT/NAME 的自定义值。虽然我们可以手动管理,但这一模式在 Django 社区中已经非常成熟,以至于有一个专门的第三方包 [dj-database-url](https://github.com/jazzband/dj-database-url) 为我们管理。方便的是,dj-database-url 已经安装好了,因为它是 [environs](https://github.com/sloria/environs)[django] 添加的帮助包之一。

这意味着我们只需一行代码就能解决所有这些问题。以下是 django_project/settings.py 的简要更新,使我们的项目尝试访问 DATABASE_URL 环境变量。

# django_project/settings.py
DATABASES = {"default": env.dj_db_url("DATABASE_URL")}

我们还需要在 .env 文件中为本地开发设置一个 DATABASE_URL 环境变量。

# .env
DEBUG=True
SECRET_KEY=imDnfLXy-8Y-YozfJmP2Rw_81YA_qx1XKl5FeY0mXyY
DATABASE_URL=sqlite:///db.sqlite3

让我们回顾一下这里发生了什么,因为一开始可能会令人困惑。对于本地开发,我们的项目会尝试查找包含环境变量的 .env 文件。如果找到,就会使用它们,这就是为什么它们是本地默认值。

在生产环境中,我们将 .env 文件包含在 .gitignore 中,因此 Heroku 除非我们手动设置环境变量,否则不会知道它们。Heroku 会自动创建一个 DATABASE_URL 环境变量,其中包含生产数据库的配置,因此会使用它。

我们还需要安装 Psycopg,这是一个数据库适配器,让像我们这样的 Python 应用可以与 PostgreSQL 数据库通信。你可以在 Windows 上使用 pip 安装,但如果你使用的是 macOS,则需要先通过 Homebreww](https://brew.sh/) 安装 PostgreSQL。

# Windows
(.venv) $ python -m pip install "psycopg[binary]"==3.2.1

# macOS
(.venv) $ brew install postgresql
(.venv) $ python3 -m pip install "psycopg[binary]"==3.2.1

我们使用的是二进制版本,因为这是开始使用 Psycopg 的最快方式。

Gunicorn 和 Procfile

由于 Django 的默认开发服务器 runserver 明确不适合生产环境,我们必须选择一个生产就绪的 WSGI 服务器。Gunicorn 是最流行且最容易配置的选项之一。它可以同时处理多个请求,同时具有可扩展性、稳定性、可靠性,并且与生产 Web 服务器兼容。

使用 pip 安装 Gunicorn。由于我们使用的是 PaaS,不需要额外的配置步骤。

(.venv) $ python -m pip install gunicorn==22.0.0

Heroku 依赖一个专有的 Procfile 文件,该文件提供关于如何在他们的堆栈中运行应用的说明。在文本编辑器中,在基础目录中创建一个名为 Procfile 的新文件。我们的项目只需要一行配置,告诉 Heroku 使用 Gunicorn 作为 WSGI 服务器,指定 WSGI 配置文件的位置为 django_project.wsgi,最后,--log-file - 标志使所有日志消息对我们可见。

# Procfile
web: gunicorn django_project.wsgi --log-file -

requirements.txt

我们差不多完成了部署清单的实现。在部署到 Heroku 之前,最后一步是更新 requirements.txt 文件。毕竟,为了部署我们安装了以下新包:whitenoise、environs、psycopg 和 gunicorn。

使用 pip freeze 命令和 > 运算符将虚拟环境信息输出到 requirements.txt 文件中。

(.venv) $ pip freeze > requirements.txt

requirements.txt 文件将出现在根目录中,包含所有已安装的包及其依赖项。在撰写本书时,我的列表如下所示:

# requirements.txt
asgiref==3.8.1
black==24.4.2
click==8.1.7
crispy-bootstrap5==2024.2
dj-database-url)==2.2.0
dj-email-url==1.0.6
Django==5.0.6
django-cache-url==3.4.5
django-crispy-forms==2.2
environs)==11.0.0
gunicorn==22.0.0
marshmallow==3.21.3
mypy-extensions==1.0.0
packaging==24.1
pathspec==0.12.1
platformdirs==4.2.2
psycopg==3.2.1
psycopg-binary==3.2.1
python-dotenv==1.0.1
sqlparse==0.5.0
typing_extensions==4.12.2
whitenoise==6.7.0

我们可以使用 git status 检查更改,添加新文件,并提交。也可以推送到 GitHub 以备份代码更改。

(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "New updates for Heroku deployment"
(.venv) $ git push -u origin main

Heroku 设置

在 2022 年之前,Heroku 有慷慨的免费套餐,但不幸的是,现在已经没有了。公司需要花费真金白银来为你启动虚拟服务器,因此很少有托管公司再提供免费套餐了。

Heroku 的定价涉及多个功能层级,按小时计费,每月有最高限额。我们将实现的部署设置如果一直开着,每月花费 12 美元,但如果你对成本敏感并且只是为了教育目的而部署,没有理由让网站一直”在线”。你可以部署网站、分享它,然后在几天后关闭,总成本应该只有 1-2 美元。

在他们的网站上注册 Heroku 账户。填写注册表格,等待确认链接的电子邮件。这会带你去设置密码的页面。配置完成后,你将进入网站的仪表板部分。Heroku 现在要求启用多因素认证(MFA),可以通过 SalesForce 或 Google Authenticator 等工具完成。Heroku 现在还需要添加信用卡用于账户验证和支付。

注册完成后,就该安装 Heroku 的命令行界面(CLI)了,这样我们就可以从命令行部署了。目前,我们在 Newspaper 项目的本地虚拟环境中操作。我们希望全局安装 Heroku,以便所有项目都能使用。一个简单的方法是打开一个新的命令行标签页(Windows 上按 Control+t,Mac 上按 Command+t),这个标签页当前不在虚拟环境中运行。在此安装的任何内容都将是全局的。

在 Windows 上,请参阅 Heroku CLI 页面以正确安装 32 位或 64 位版本。在 Mac 上,使用包管理器 Homebreww](https://brew.sh/) 进行安装。如果你还没有安装 Homebreww](https://brew.sh/),请从 Homebreww](https://brew.sh/) 网站复制粘贴长命令到命令行中并回车。它看起来像这样:

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebreww](https://brew.sh/)/install/HEAD/install.sh)"

接下来,复制粘贴以下内容到命令行并回车,安装 Heroku CLI

$ brew tap heroku/brew && brew install heroku

安装完成后,你可以关闭新的命令行标签页,返回到激活了 newspaper 虚拟环境的原始标签页。

输入 heroku login 命令并回车。然后按任意键打开浏览器窗口,使用你的电子邮件、密码和刚设置的双因素认证登录。

(.venv) > heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/....

!Heroku 登录

登录完成后,我们就准备好了!

使用 Heroku 部署

与 Heroku 交互有两种方式:通过 CLI(命令行界面)或网站。CLI 速度更快,我们将在这里使用,但网站的图形化特性对于 Heroku 新手来说很有帮助。

第一步是使用 heroku create 从命令行创建一个新的 Heroku 应用。Heroku 会为我们的应用生成一个随机名称,在我的例子中是 fathomless-hamlet-26076。你的名称会不同。

(.venv) $ heroku create
Creating app... done, afternoon-wave-82807
https://afternoon-wave-82807-b672795cd97e.herokuapp.com/ | https://git.heroku.com/afternoon-wave-82807.git

heroku create 命令还会为我们的应用创建一个名为 heroku 的专用 Git 远程仓库。输入 git remote -v 可以看到。

(.venv) $ git remote -v
heroku  https://git.heroku.com/afternoon-wave-82807.git (fetch)
heroku  https://git.heroku.com/afternoon-wave-82807.git (push)

下一步是在 Heroku 上创建一个 PostgreSQL 数据库。有不同的 Postgres 层级可供不同的用例使用。五个计划层级是:Essential、Standard、Premium、Private 和 Shield。付费越高,容忍的停机时间越短。对于我们的用例,最低层级 Essential 已经足够了。运行以下命令为我们的项目创建一个新的 Essential Postgres 数据库。

(.venv) $ heroku addons:create heroku-postgresql:essential-0
Creating heroku-postgresql:essential-0 on afternoon-wave-82807...
~$0.007/hour (max $5/month)
Database should be available soon
postgresql-sinuous-77120 is being created in the background. The app will restart when complete...
Use heroku addons:info postgresql-sinuous-77120 to check creation progress
Use heroku addons:docs heroku-postgresql to view documentation

数据库可能需要一点时间来配置,这种情况下你可以等几分钟,然后运行命令”check creation progress”。确保数据库名称与你的项目匹配。

(.venv) $ heroku addons:info postgresql-sinuous-77120
=== postgresql-sinuous-77120
Attachments:  afternoon-wave-82807::DATABASE
Installed at: Tue Jul 02 2024 10:57:17 GMT-0400 (Eastern Daylight Time)
Max Price:    $5/month
Owning app:   afternoon-wave-82807
Plan:         heroku-postgresql:essential-0
Price:        ~$0.007/hour
State:        created

如果你运行 heroku config,它会显示 Heroku 上设置的所有配置变量。目前只有 DATABASE_URL,其中包含连接到生产 Postgres 数据库的信息。

(.venv) $ heroku config
=== afternoon-wave-82807 Config Vars
DATABASE_URL: postgres://u1k...us-east-1.rds.amazonaws.com:5432/d11ac0v0inabta

在 Heroku 网站仪表板中选择你的项目,点击导航栏中的”Settings”。在”Config Vars”下,你可以看到 DATABASE_URL 已经设置。

Heroku 仪表板配置

我们的本地 .env 文件中还有另外两个项目:DEBUGSECRET_KEY。我们需要在 Heroku 上手动设置这两个变量,可以在 Web 界面中设置,也可以在命令行中设置。首先是 DEBUG,应该设置为 False

(.venv) $ heroku config:set DEBUG=False
Setting DEBUG and restarting afternoon-wave-82807... done, v6
DEBUG: False

接下来是 SECRET_KEY。如果通过命令行设置,请确保用引号 "" 包裹。

(.venv) $ heroku config:set SECRET_KEY="SECRET_KEY=imDnfLXy-8Y-YozfJmP2Rw_81YA_qx1XKl5FeY0mXyY"
Setting SECRET_KEY and restarting afternoon-wave-82807... done, v7
SECRET_KEY: SECRET_KEY=imDnfLXy-8Y-YozfJmP2Rw_81YA_qx1XKl5FeY0mXyY

最好仔细检查生产环境变量是否设置正确。从命令行使用 heroku config 命令。

(.venv) $ heroku config
=== afternoon-wave-82807 Config Vars
DATABASE_URL: postgres://u1k...us-east-1.rds.amazonaws.com:5432/d11ac0v0inabta
DEBUG:        False
SECRET_KEY:   SECRET_KEY=imDnfLXy-8Y-YozfJmP2Rw_81YA_qx1XKl5FeY0mXyY

你也可以查看 Web 仪表板。

Heroku 仪表板更新后的配置

现在是时候用命令 git push heroku main 将代码推送到 Heroku 了。如果我们只输入 git push origin main,代码会被推送到 GitHub,而不是 Heroku。加上 heroku 则将代码发送到 Heroku。

(.venv) $ git push heroku main
Enumerating objects: 339, done.
Counting objects: 100% (339/339), done.
Delta compression using up to 10 threads
Compressing objects: 100% (333/333), done.
Writing objects: 100% (339/339), 798.15 KiB | 14.25 MiB/s, done.
Total 339 (delta 39), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (39/39), done.
remote: Updated 569 paths from 2ebafa9
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Building on the Heroku-22 stack
...
remote:
https://afternoon-wave-82807-b672795cd97e.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/afternoon-wave-82807.git
 * [new branch]      main -> main

这个命令会产生大量来自 Heroku 的输出,第一次可能需要一些时间。我们正在将代码推送到 Heroku,它会重新构建我们 Django 项目的生产版本。你会看到它安装 requirements.txt 中的每个项目以及其他操作。

最后一步是启动一个 Dyno,这是 Heroku 对我们应用轻量级容器的术语。我们需要至少一个正在运行的才能使网站上线。如果流量开始激增,我们可以为项目添加更多 dyno,Heroku 会处理所有基础设施。对于这样的小项目,我推荐 Basic Dyno,每小时 0.01 美元,每月最高 7 美元。

我们将使用 Heroku CLI 启动一个 dyno,但也可以通过 Web 界面管理 dyno。CLI 的一般语法是以 heroku 开头,ps 是许多与 dyno 相关的命令的前缀,ps:scale 用于增加运行某个进程的 dyno 数量。因此,下面的命令告诉 Heroku 为我们的网站运行一个 dyno。

(.venv) $ heroku ps:scale web=1
Scaling dynos... done, now running web at 1:Basic

如果我们一直运行,项目的总成本是每月 12 美元:Postgres 数据库每月 5 美元,Dyno 每月 7 美元。Heroku 按小时计费,所以你总是可以部署网站并在几天后关闭,这样应该只花费几分钱。

我们完成了!最后一步是确认我们的应用已经上线并在线。如果你使用 heroku open 命令,浏览器会打开一个新标签页,显示你的应用 URL:

(.venv) $ heroku open

!生产环境主页

Newspaper 网站已经上线,但如果你尝试使用,很快会发现一些问题。首先,没有文章或评论!那是因为我们还需要配置在 Heroku 上运行的生产 PostgreSQL 数据库。现在就来处理。要在 Heroku 上运行 Django 命令(而不是本地),我们使用前缀 heroku run。因此,要使用初始设置迁移数据库,我们运行以下命令:

(.venv) $ heroku run python manage.py migrate
Running python manage.py migrate on afternoon-wave-82807... up, run.2790 (Basic)
Operations to perform:
  Apply all migrations: accounts, admin, articles, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying accounts.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying articles.0001_initial... OK
  Applying articles.0002_comment... OK
  Applying sessions.0001_initial... OK

现在,创建一个超级用户账户以访问管理后台。

(.venv) $ heroku run python manage.py createsuperuser
Running python manage.py createsuperuser on afternoon-wave-82807... up, run.7422 (Basic)
Username: wsv
Email address: will@learndjango.com
Password:
Password (again):
Superuser created successfully.

导航到已部署网站的管理后台部分,使用超级用户凭据登录,并添加一些文章和评论。

!生产环境管理仪表板

它们会显示在在线网站上。你还可以创建用户账户,并通过重置密码来确认用户认证流程是否正常工作。

对于将来对生产网站的更新,模式如下:

  • 进行本地代码更改并通过 Git 保存
  • 使用 git push origin main 部署到 GitHub
  • 使用 git push heroku main 将代码推送到 Heroku

如果你想移除托管的网站,请登录 Heroku 仪表板,点击应用名称。点击顶部导航栏中的 Settings 链接,然后滚动到页面底部的 Delete App,点击”Delete App…“按钮。系统会要求你再次输入完整的应用名称以确认永久删除。

!Heroku 删除应用

另一个提示:你可以随时按 Ctrl + d 退出 Heroku CLI

附加安全步骤

保护生产网站的安全措施几乎是无穷无尽的。我们的生产清单涵盖了基本内容,但如果你愿意采取更多步骤,还有更多可以做的。

首先,将 [ALLOWED_HOSTS](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-ALLOWED_HOSTS)CSRF_TRUSTED_ORIGINS 更新为使用项目的确切生产 URL。

其次,运行 Django 的管理命令 python manage.py check --deploy,它会运行几个围绕部署的自动化检查。要运行该命令,你需要添加前缀 heroku run,即 heroku run python manage.py check --deploy。你现在知道如何参考 Django 文档并更新本地和生产环境变量以满足检查要求。

总结

我们刚刚涵盖了大量新内容,所以你可能会感到不知所措。这很正常。正确配置网站部署涉及很多步骤,好消息是,这套生产设置清单几乎适用于每个 Django 项目。不要担心记住所有步骤,使用部署清单即可!

对于新手来说,另一个大的绊脚石是适应本地环境和生产环境之间的区别。你可能会忘记将代码更改推送到生产环境,然后花上几分钟甚至几小时思考为什么更改在你的网站上没有生效。或者更糟糕的是,你修改了本地的 SQLite 数据库,却期望它们神奇地出现在生产环境的 PostgreSQL 数据库中。这是学习过程的一部分,但 Django 已经使其比原本顺畅得多。你现在已经掌握了足够的知识,可以自信地通过 PaaS 将任何 Django 项目部署上线。