如何使用Docker构建Django和Gunicorn应用程序
介绍
Django 是一个强大的 Web 框架,可以帮助您快速启动 Python 应用程序。 它包括几个方便的功能,例如 对象关系映射器 、用户身份验证和可自定义的应用程序管理界面。 它还包括一个 缓存框架 并通过其 URL Dispatcher 和 模板系统 鼓励干净的应用程序设计。
在本教程中,您将学习如何使用 Docker 容器构建可扩展且可移植的 Django Polls 应用程序。 开箱即用,Django 应用程序需要进行一些修改才能在容器内有效运行,例如记录到标准输出流并通过传递到容器的环境变量来配置自身。 此外,将 JavaScript 和 CSS 样式表等静态资产卸载到对象存储中,您可以在多容器环境中简化和集中管理这些文件。
您将在示例 Django Polls 应用程序上实施这些修改(受 Twelve-Factor 方法论的启发,用于构建可扩展的云原生 Web 应用程序)。 然后,您将构建应用程序映像并使用 Docker 运行容器化应用程序。
在本教程结束时,您将容器化 如何设置可扩展的 Django 应用程序 中的设置。 在本系列后续教程中,您将学习如何使用 Docker Compose 将 Django 容器与 Nginx 反向代理配对,并将此架构部署到 Kubernetes 集群。
强烈建议您阅读本教程以了解您对应用程序所做的更改,但如果您想跳过,您可以从 polls-docker 分支 获取修改后的代码投票应用程序 GitHub 存储库。
先决条件
要学习本教程,您需要:
- 一个 Ubuntu 18.04 服务器,按照 Initial Server Setup guide 进行设置。
- 按照 如何在 Ubuntu 18.04 上安装和使用 Docker 的步骤 1 和 2 安装在您的服务器上的 Docker。 确保将您的用户添加到
docker
组,如步骤 2 中所述。 - DigitalOcean Space 用于存储 Django 项目的静态文件和该空间的一组访问密钥。 要了解如何创建空间,请参阅 如何创建空间 产品文档。 要了解如何为空间创建访问密钥,请参阅 使用访问密钥共享对空间的访问。 只需稍作更改,您就可以使用任何与 S3 兼容的对象存储服务。
- 一个 DigitalOcean 托管的 PostgreSQL 集群。 要了解如何创建集群,请参阅 DigitalOcean Managed Databases 产品文档。 只需稍作更改,您就可以使用 Django 支持的任何数据库 。
第 1 步 — 创建 PostgreSQL 数据库和用户
首先,我们将从 Ubuntu 实例连接到 PostgreSQL 服务器。 然后,我们将为 Django 应用程序创建一个 PostgreSQL 数据库和用户,并配置数据库以有效地与 Django 一起工作。
在我们从我们的 Ubuntu 机器(不是应用程序容器)连接到数据库之前,我们需要从 Ubuntu 存储库中安装 postgresql-client
包。 先更新本地的apt
包索引,然后下载安装包:
sudo apt update sudo apt install postgresql-client
当提示开始下载和安装软件包时,点击 Y
,然后点击 ENTER
。
现在您已经安装了客户端,我们将使用它为我们的 Django 应用程序创建一个数据库和数据库用户。
首先,通过从 Cloud 控制面板 导航到 Databases,然后单击进入您的数据库,为您的集群获取 Connection Parameters。 您应该会看到一个 Connection Details 框,其中包含您的集群的一些 Connection parameters。 记下这些。
返回命令行,使用这些凭据和我们刚刚安装的 psql
PostgreSQL 客户端登录到您的集群:
psql -U username -h host -p port -d database --set=sslmode=require
出现提示时,输入 Postgres 用户名旁边显示的密码,然后点击 ENTER
。
您将收到一个 PostgreSQL 提示,您可以从中管理数据库。
首先,为您的项目创建一个名为 polls
的数据库:
CREATE DATABASE polls;
注意: 每个 Postgres 语句都必须以分号结尾,因此如果遇到问题,请确保您的命令以分号结尾。
我们现在可以切换到 polls
数据库:
\c polls;
接下来,为项目创建一个数据库用户。 确保选择安全密码:
CREATE USER sammy WITH PASSWORD 'password';
现在,我们将为刚刚创建的用户修改一些连接参数。 这将加快数据库操作,从而不必在每次建立连接时都查询和设置正确的值。
我们将默认编码设置为 UTF-8
,这是 Django 所期望的。 我们还将默认事务隔离方案设置为“已提交读”,这会阻止来自未提交事务的读取。 最后,我们正在设置时区。 默认情况下,我们的 Django 项目将设置为使用 UTC
。 这些都是来自 Django 项目本身 的建议。
在 PostgreSQL 提示符下输入以下命令:
ALTER ROLE sammy SET client_encoding TO 'utf8'; ALTER ROLE sammy SET default_transaction_isolation TO 'read committed'; ALTER ROLE sammy SET timezone TO 'UTC';
现在我们可以让我们的新用户访问管理我们的新数据库:
GRANT ALL PRIVILEGES ON DATABASE polls TO sammy;
完成后,键入以下命令退出 PostgreSQL 提示:
\q
正确配置的 Django 应用程序现在可以连接并管理此数据库。 在下一步中,我们将从 GitHub 克隆 Polls 应用程序代码,并明确定义其 Python 包依赖项。
第 2 步 — 克隆应用程序存储库并声明依赖项
要开始容器化我们的 Django Polls 应用程序的过程,我们将首先克隆 django-polls 存储库,其中包含 Django 项目的 tutorial 的完整代码民意调查应用程序。
登录到您的服务器,创建一个名为 polls-project
的目录并使用 git
从 GitHub 克隆 django-polls
存储库:
mkdir polls-project cd polls-project git clone https://github.com/do-community/django-polls.git
访问 django-polls
目录并列出存储库内容:
cd django-polls ls
OutputLICENSE README.md manage.py mysite polls templates
您应该看到以下对象:
manage.py
:用于操作应用程序的主要命令行实用程序。polls
:包含polls
应用程序代码。mysite
:包含 Django 项目范围代码和设置。templates
:包含管理界面的自定义模板文件。
要了解有关项目结构和文件的更多信息,请参阅 Django 官方文档中的创建项目。
在这个目录中,我们还将创建一个名为 requirements.txt
的文件,该文件将包含 Django 应用程序的 Python 依赖项。
在您选择的编辑器中打开一个名为 requirements.txt
的文件,然后粘贴以下 Python 依赖项:
民意调查项目/django-polls/requirements.txt
boto3==1.9.252 botocore==1.12.252 Django==2.2.6 django-storages==1.7.2 docutils==0.15.2 gunicorn==19.9.0 jmespath==0.9.4 psycopg2==2.8.3 python-dateutil==2.8.0 pytz==2019.3 s3transfer==0.2.1 six==1.12.0 sqlparse==0.3.0 urllib3==1.25.6
在这里,我们安装了 Django、用于将静态资产卸载到对象存储的 django-storages
插件、gunicorn
WSGI 服务器、psycopg2
PostgreSQL 适配器,以及一些额外的依赖包。 请注意,我们明确列出并版本化了我们的应用程序所需的每个 Python 包。
保存并关闭文件。
现在我们已经克隆了应用程序并定义了它的依赖项,我们可以继续修改它以实现可移植性。
第 3 步 — 使 Django 可通过环境变量进行配置
十二因素应用程序 方法中最重要的建议之一是从应用程序的代码库中提取硬编码配置。 这使您可以通过修改环境变量轻松地在运行时更改应用程序的行为。 Docker 和 Kubernetes 都建议使用这种配置容器的方法,因此我们将调整应用程序的设置文件以使用这种模式。
我们的 Django 项目 (django-polls/mysite/settings.py
) 的主要设置文件是一个 Python 模块,它使用本机数据结构来配置应用程序。 默认情况下,文件中的大多数值都是硬编码的,这意味着您必须编辑配置文件才能更改应用程序行为。 我们可以使用 Python 的 os
模块中的 getenv
函数来配置 Django 从本地环境变量中读取配置参数。
为此,我们将遍历 settings.py
,并通过调用 os.getenv
替换我们想要在运行时设置的每个变量的硬编码值。 os.getenv
函数从提供的环境变量名称中读取值。 如果未设置环境变量,您可以选择提供带有默认值的第二个参数。
这允许我们像这样设置变量:
民意调查项目/django-polls/mysite/settings.py
. . . SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') . . . DEBUG = os.getenv('DJANGO_DEBUG', False) . . .
对于 SECRET_KEY
,Django 将寻找一个名为 DJANGO_SECRET_KEY
的环境变量。 由于这不应该是硬编码的并且需要在我们的应用程序服务器中保持相同,因此我们希望在外部设置它而没有备用值。 如果我们不提供这个,我们希望应用程序失败,因为如果我们的应用程序的不同副本使用不同的密钥,它可能会导致问题。
对于 DEBUG
,Django 将寻找一个名为 DJANGO_DEBUG
的环境变量。 但是,这一次,我们提供了一个默认值,如果未设置变量,该默认值将用作后备。 在这种情况下,如果没有提供任何值,我们选择将 DEBUG
设置为 False
,这样我们就不会意外泄漏敏感信息,除非有意定义变量并将其设置为 [ X198X]…
要应用此技术,请在您选择的编辑器中打开 polls-project/django-polls/mysite/settings.py
文件,然后浏览它,使用提供的默认值外部化以下变量:
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
DEBUG = os.getenv('DEBUG', False)
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1').split(',')
对于 ALLOWED_HOSTS
,我们获取 DJANGO_ALLOWED_HOSTS
环境变量,并使用 ,
作为分隔符将其拆分为 Python 列表。 如果未设置变量,则 ALLOWED_HOSTS
设置为 127.0.0.1
。
修改上述变量后,导航到 DATABASES
变量并进行如下配置:
民意调查项目/django-polls/mysite/settings.py
. . . # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.{}'.format( os.getenv('DATABASE_ENGINE', 'sqlite3') ), 'NAME': os.getenv('DATABASE_NAME', 'polls'), 'USER': os.getenv('DATABASE_USERNAME', 'myprojectuser'), 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'password'), 'HOST': os.getenv('DATABASE_HOST', '127.0.0.1'), 'PORT': os.getenv('DATABASE_PORT', 5432), 'OPTIONS': json.loads( os.getenv('DATABASE_OPTIONS', '{}') ), } } . . .
这将使用环境变量设置 default
数据库参数。
对于 DATABASES['default']['OPTIONS']
,我们使用 json.loads
反序列化通过 DATABASE_OPTIONS
环境变量传入的 JSON 对象。 大多数情况下,将环境变量解释为简单的字符串会使转换为 Django 设置更容易阅读。 但是,在这种情况下,能够传入任意数据结构是很有价值的。 每个数据库引擎都有一组独特的有效选项,因此能够使用适当的参数对 JSON 对象进行编码为我们提供了更大的灵活性,但代价是一些易读性。
要使用 json
库,请将其导入 settings.py
的顶部:
民意调查项目/django-polls/mysite/settings.py
""" Django settings for mysite project. Generated by 'django-admin startproject' using Django 2.1. For more information on this file, see https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os import json . . .
另一个需要特别注意的区域是 DATABASES['default']['NAME']
。 对于大多数数据库引擎,这是关系数据库管理系统中的数据库名称。 另一方面,如果您使用 SQLite,NAME
用于指定数据库文件,因此请务必相应地设置此参数。
由于 settings.py
文件是 Python 代码,因此有许多不同的方法可以处理从环境中读取值。 我们在这里使用的方法只是从代码库外部化配置的一种可能技术。
在这一步中,我们以通用和可移植的方式配置了主要的 Django 设置变量,包括数据库参数。 在接下来的步骤中,我们将继续为 Javascript 和 CSS 样式表等静态文件配置设置,我们将集中这些文件并将其卸载到与 S3 兼容的对象存储服务。
第 4 步 — 卸载静态资产
在生产环境中运行多个 Django 容器时,在整个运行容器中维护特定版本的静态资产和文件可能很麻烦。 为了简化这种架构,我们可以将所有共享元素和状态卸载到外部存储。 我们可以将这些资产的访问实现为网络可访问的服务,而不是尝试在副本之间保持这些项目同步或实施备份和加载例程以确保数据在本地可用。
在最后一步中,我们配置了 Django,以便我们可以通过环境变量传入数据库连接参数。 在这一步中,我们将对对象存储服务执行相同的操作,我们将使用它来存储将由 Django 容器共享的静态资产。
django-storages 包提供了远程存储后端(包括与 S3 兼容的对象存储),Django 可以使用它来卸载文件。 我们将配置投票应用程序以使用 django-storages
将静态文件上传到 DigitalOcean 空间,如 如何使用 DigitalOcean 托管数据库和空间 设置可扩展的 Django 应用程序的第 7 步中所述. 在本指南中,我们将使用 DigitalOcean Spaces,但您可以使用任何与 S3 兼容的对象存储提供程序。
首先,我们将对我们在前面的步骤中更改过的同一个 django-polls/mysite/settings.py
文件进行一些修改。
首先打开 mysite/settings.py
文件以编辑 storages
应用程序并将其附加到 Django 的 INSTALLED_APPS
列表中:
民意调查项目/django-polls/mysite/settings.py
. . . INSTALLED_APPS = [ . . . 'django.contrib.staticfiles', 'storages', ] . . .
storages
应用程序通过我们在步骤 1 中定义的 requirements.txt
文件中的 django-storages
安装。
现在,找到文件底部的 STATIC_URL
变量,并将其替换为以下块:
民意调查项目/django-polls/mysite/settings.py
. . . # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ # Moving static assets to DigitalOcean Spaces as per: # how-to-set-up-object-storage-with-django AWS_ACCESS_KEY_ID = os.getenv('STATIC_ACCESS_KEY_ID') AWS_SECRET_ACCESS_KEY = os.getenv('STATIC_SECRET_KEY') AWS_STORAGE_BUCKET_NAME = os.getenv('STATIC_BUCKET_NAME') AWS_S3_ENDPOINT_URL = os.getenv('STATIC_ENDPOINT_URL') AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', } AWS_LOCATION = 'static' AWS_DEFAULT_ACL = 'public-read' STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' STATIC_URL = '{}/{}/'.format(AWS_S3_ENDPOINT_URL, AWS_LOCATION) STATIC_ROOT = 'static/'
我们硬编码以下配置变量:
STATICFILES_STORAGE
:设置 Django 将用于卸载静态文件的存储后端。 这个S3Boto3Storage
后端应该与任何 S3 兼容的后端一起使用,包括 DigitalOcean Spaces。AWS_S3_OBJECT_PARAMETERS
设置静态文件的缓存控制头。AWS_LOCATION
:在对象存储桶中定义一个名为static
的目录,所有静态文件都将放置在该目录中。- ``AWS_DEFAULT_ACL
: Defines the access control list (ACL) for the static files. Setting it to
public-read` 确保最终用户可以公开访问这些文件。 STATIC_URL
:指定 Django 在为静态文件生成 URL 时应该使用的基本 URL。 在这里,我们结合端点 URL 和静态文件子目录来构造静态文件的基本 URL。STATIC_ROOT
:指定在将静态文件复制到对象存储之前本地收集静态文件的位置。
为了保持灵活性和可移植性,我们将许多参数设置为在运行时使用环境变量进行配置,就像我们之前所做的那样。 这些包括:
AWS_ACCESS_KEY_ID
:由STATIC_ACCESS_KEY_ID
环境变量设置。 DigitalOcean Spaces 访问密钥标识符。AWS_SECRET_ACCESS_KEY
:由STATIC_SECRET_KEY
设置。 DigitalOcean Spaces 密钥。AWS_STORAGE_BUCKET_NAME
:由STATIC_BUCKET_NAME
设置。 Django 将上传资产的对象存储桶。AWS_S3_ENDPOINT_URL
:由STATIC_ENDPOINT_URL
设置。 用于访问对象存储服务的端点 URL。 对于 DigitalOcean Spaces,这将类似于https://nyc3.digitaloceanspaces.com
,具体取决于您的 Spaces 存储桶所在的区域。
完成对 settings.py
的更改后,保存并关闭文件。
从现在开始,当您运行 manage.py collectstatic
来组装项目的静态文件时,Django 会将这些文件上传到远程对象存储。 Django 现在也被配置为从这个对象存储服务中提供静态资产。
此时,如果您使用的是 DigitalOcean Space,您可以选择为您的 Space 启用 CDN,这将通过在地理分布的边缘服务器网络中缓存 Django 项目的静态文件来加快交付速度。 您还可以选择为您的空间配置自定义子域。 要了解有关 CDN 的更多信息,请参阅 使用 CDN 加速静态内容交付 。 配置 CDN 超出了本教程的范围,但步骤与 如何使用 DigitalOcean 托管数据库和空间 设置可扩展的 Django 应用程序的 启用 CDN 部分中的步骤非常相似.
在下一步中,我们将对 settings.py
进行最后一组更改,这将启用 Django 记录到 STDOUT 和 STDERR 以便 Docker 引擎可以拾取这些流并使用 [X203X 进行检查]。
第 5 步 — 配置日志记录
默认情况下,Django 在运行开发 HTTP 服务器或 DEBUG
选项设置为 True
时将信息记录到标准输出和标准错误。 但是,当 DEBUG
设置为 False
或使用不同的 HTTP 服务器时,这两种情况在生产环境中可能都是正确的,Django 使用不同的日志记录机制。 它不会将优先级为 INFO
及以上的所有内容记录到标准流中,而是将优先级为 ERROR
或 CRITICAL
的消息发送到管理电子邮件帐户。
这在许多情况下都有意义,但在 Kubernetes 和容器化环境中,强烈建议将日志记录到标准输出和标准错误。 日志消息收集在节点文件系统上的集中目录中,并且可以使用 kubectl
和 docker
命令以交互方式访问。 这种节点级别的聚合通过允许操作团队在每个节点上运行一个进程来观察和转发日志来促进日志收集。 要利用此架构,应用程序必须将其日志写入这些标准接收器。
幸运的是,登录 Django 使用 Python 标准库中高度可配置的 logging
模块,因此我们可以定义一个字典来传递给 logging.config.dictConfig 来定义我们想要的输出和格式。 要了解有关此技术和其他配置 Django 日志记录的更多信息,请参阅 Django Logging, The Right Way。
再次在编辑器中打开 django-polls/mysite/settings.py
。
我们将首先在文件顶部添加一个额外的 import
语句,以便我们可以操作日志记录配置:
民意调查项目/django-polls/mysite/settings.py
import json import os import logging.config . . .
logging.config
导入允许我们通过将新日志配置的字典传递给 dictConfig
函数来覆盖 Django 的默认日志记录行为。
现在,导航到文件底部,并粘贴以下日志配置代码块:
民意调查项目/django-polls/mysite/settings.py
. . . # Logging Configuration # Clear prev config LOGGING_CONFIG = None # Get loglevel from env LOGLEVEL = os.getenv('DJANGO_LOGLEVEL', 'info').upper() logging.config.dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'console': { 'format': '%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(module)s %(process)d %(thread)d %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'console', }, }, 'loggers': { '': { 'level': LOGLEVEL, 'handlers': ['console',], }, }, })
在这里,我们将 LOGGING_CONFIG
设置为 None
以禁用 Django 提供的默认日志记录配置。 我们默认将 LOGLEVEL
设置为 INFO
,但请检查 DJANGO_LOGLEVEL
环境变量,以便我们可以根据需要进行覆盖。
最后,我们使用 dictConfig
函数使用 logging.config
模块设置新的配置字典。 在字典中,我们使用 formatters
定义文本格式,通过设置 handlers
定义输出,并使用 loggers
配置应该发送到每个处理程序的消息。
这是一个相当简单的配置,允许您使用名为 DJANGO_LOGLEVEL
的环境变量指定日志记录严重性级别,然后将所有处于或高于该级别的消息记录到标准流。 有关 Django 日志记录机制的深入讨论,请参阅 Django 官方文档中的 Logging。
有了这个配置,当我们容器化应用程序时,Docker 会通过 docker logs
命令暴露这些日志。 同样,Kubernetes 将捕获输出并通过 kubectl logs
命令将其公开。
我们对 Django Polls 应用程序的代码修改到此结束。 在下一步中,我们将通过编写应用程序的 Dockerfile 来开始容器化过程。
第 6 步 — 编写应用程序 Dockerfile
在这一步中,我们将定义将运行我们的 Django 应用程序的容器镜像和为其提供服务的 Gunicorn WSGI 服务器。 它涉及通过定义运行时环境、安装应用程序及其依赖项以及完成一些基本配置来构建容器映像。 虽然有许多可能的方法可以将应用程序封装在容器映像中,但在此步骤中遵循的做法会产生精简、流线型的应用程序映像。
选择合适的父图像
构建容器映像时必须做出的第一个重大决定是构建的基础。 容器镜像既可以从 SCRATCH
构建,表示一个空的文件系统,也可以从现有的容器镜像构建。 有许多不同的基本容器镜像可用,每个镜像都定义一个文件系统并提供一组独特的预安装包。 基于普通 Linux 发行版(如 Ubuntu 18.04)的映像提供了一个通用的操作环境,而更专业的映像通常包括用于特定编程语言的通用库和工具。
只要有可能,使用来自 Docker 的官方存储库 之一的图像作为基础通常是个好主意。 这些镜像已经过 Docker 验证以遵循最佳实践,并定期更新以进行安全修复和改进。
由于我们的应用程序是使用 Django 构建的,因此具有标准 Python 环境的映像将提供坚实的基础,并包含我们开始使用的许多工具。 Python 的官方 Docker 存储库提供 多种基于 Python 的映像 ,每个映像都在操作系统之上安装一个 Python 版本和一些常用工具。
虽然适当的功能级别取决于您的用例,但基于 Alpine Linux 的图像通常是一个可靠的起点。 Alpine Linux 为运行应用程序提供了一个强大但最小的操作环境。 它的默认文件系统非常小,但包括一个完整的包管理系统和相当广泛的存储库,使添加功能变得简单。
注意:您可能已经注意到在 Python图像标签列表[X77X]中,每个图像都有多个标签可用。 Docker 标签是可变的,维护者将来可以将相同的标签重新分配给不同的镜像。 结果,许多维护者提供了具有不同程度特异性的标签集,以允许不同的用例。 例如,标签 3-alpine
用于指向最新的 Alpine 版本上的最新可用 Python 3 版本,因此当 Python 或 Alpine 的新版本发布时,它将重新分配给不同的图像。 为了使图像构建更具确定性,最好使用您可以找到的最具体的标签来用于您想要使用的图像。
在本指南中,我们将使用标记为 3.7.4-alpine3.10
的 Python 图像作为 Django 应用程序的父图像。 我们使用 FROM
指令在 Dockerfile
中指定父映像的存储库和标签。
首先,导航出 django-polls
目录。
cd ..
然后,在您选择的编辑器中打开一个名为 Dockerfile
的文件。 粘贴以下父图像定义:
民意调查项目/Dockerfile
FROM python:3.7.4-alpine3.10
这定义了我们为运行应用程序而构建的自定义 Docker 映像的起点。
添加说明以设置应用程序
一旦你选择了一个父镜像,你就可以开始添加安装依赖项的说明,复制我们的应用程序文件,并设置运行环境。 这个过程通常反映了您为应用程序设置服务器所采取的步骤,其中有一些关键差异来解释容器抽象。
在 FROM
行之后,粘贴以下 Dockerfile 代码块:
民意调查项目/Dockerfile
. . . ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000
让我们回顾一下这些说明来解释一些不太明显的选择。 要了解有关为 Django 应用程序构建生产就绪 Dockerfile 的更多信息,请参阅 A Production-Ready Dockerfile for your Django App。
首先 Docker 会将 requirements.txt
文件复制到 /app/requirements.txt
以便我们的应用程序的依赖项在映像的文件系统上可用。 我们将使用它来安装应用程序运行所需的所有 Python 包。 我们将依赖文件作为与我们代码库的其余部分分开的步骤进行复制,以便 Docker 可以缓存包含依赖文件的镜像层。 只要 requirements.txt
文件在构建之间没有变化,Docker 就可以重用缓存层而不是重新构建它,从而加快进程。
接下来,我们有一个 RUN
指令,它执行一长串命令,每个命令都使用 Linux &&
运算符链接在一起。 总而言之,这些命令:
- 使用 Alpine 的
apk
包管理器安装 PostgreSQL 开发文件和基本构建依赖项 - 创建虚拟环境
- 使用
pip
安装requirements.txt
中列出的 Python 依赖项 - 通过分析已安装的 Python 包的需求,编译出运行时需要的包列表
- 卸载任何不需要的构建依赖项
由于 Docker 构建镜像层的方式,我们将命令链接在一起,而不是在单独的 RUN
步骤中执行每个命令。 对于每个 ADD
、COPY
和 RUN
指令,Docker 在现有文件系统之上创建一个新的镜像层,执行指令,然后保存结果层。 这意味着压缩 RUN
指令中的命令将导致更少的图像层。
一旦项目被写入图像层,就不能在后续层中将其删除以减小图像大小。 如果我们安装了构建依赖项,但想在应用程序设置完成后删除它们,我们需要在同一指令中这样做以减小图像大小。 在这个 RUN
命令中,我们安装构建依赖项,使用它们构建应用程序的包,然后使用 apk del
删除它们。
在 RUN
指令之后,我们使用 ADD
复制应用程序代码,并使用 WORKDIR
将图像的工作目录设置为我们的代码目录。
然后,我们使用 ENV
指令设置两个环境变量,这两个环境变量将在从我们的图像生成的容器中可用。 第一条指令将 VIRTUAL_ENV
设置为 /env
,第二条指令修改 PATH
变量以包含 /env/bin
目录。 这两行模拟了获取 /env/bin/activate
脚本的结果,这是激活虚拟环境的传统方法。
最后,我们使用 EXPOSE
通知 Docker 容器将在运行时侦听端口 8000
。
至此,Dockerfile
已接近完成。 我们只需要定义在使用镜像启动容器时将运行的默认命令。
定义默认命令
Docker 镜像的默认命令决定了在容器启动时会发生什么,而无需明确提供要执行的命令。 ENTRYPOINT
和 CMD
指令可以单独使用或串联使用,以在 Dockerfile
中定义默认命令。
当 ENTRYPOINT
和 CMD
都定义时,ENTRYPOINT
定义将由容器运行的可执行文件,而 CMD
代表默认参数列表那个命令。 用户可以通过在命令行上附加替代参数来覆盖默认参数列表:docker run <image> <arguments>
。 在这种格式下,用户将无法轻松覆盖 ENTRYPOINT
命令,因此 ENTRYPOINT
命令通常设置为脚本,该脚本将设置环境并根据参数列表执行不同的操作它接收。
单独使用时,ENTRYPOINT
配置容器的可执行文件,但不定义默认参数列表。 如果只设置了CMD
,它将被解释为默认的命令和参数列表,可以在运行时被覆盖。
在我们的镜像中,我们希望容器默认使用 gunicorn
应用服务器运行我们的应用程序。 我们传递给 gunicorn
的参数列表不需要在运行时进行配置,但我们希望能够在需要调试或执行管理任务(如收集静态资产或初始化数据库)时轻松运行其他命令)。 考虑到这些要求,我们可以使用 CMD
来定义不带 ENTRYPOINT
的默认命令。
CMD
指令可以使用以下任何格式定义:
CMD ["argument 1", "argument 2", . . . ,"argument n"]
:参数列表格式(用于定义ENTRYPOINT
的默认参数列表)CMD ["command", "argument 1", "argument 2", . . . ,"argument n"]
:exec
格式CMD command "argument 1" "argument 2" . . . "argument n"
:外壳格式
第一种格式仅列出参数并与 ENTRYPOINT
结合使用。 其他两种格式指定命令及其参数,但有一些关键区别。 推荐的exec
格式直接执行命令,传入参数列表,不做shell处理。 另一方面,shell 格式将整个列表传递给 sh -c
。 例如,如果您需要在命令中替换环境变量的值,这是必要的,但通常被认为不太可预测。
出于我们的目的,我们的 Dockerfile
中的最终指令如下所示:
民意调查项目/Dockerfile
. . . CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi:application"]
默认情况下,使用该镜像的容器将执行绑定到 localhost 端口 8000
的 gunicorn
和 3 个 worker,并运行 wsgi.py
文件中的 application
函数mysite
目录。 您可以选择在运行时提供命令来执行不同的进程,而不是 gunicorn
。
此时,您可以使用 docker build
构建您的应用映像,并使用 docker run
在您的机器上运行容器。
构建 Docker 镜像
默认情况下,docker build
命令在当前目录中查找 Dockerfile
以查找其构建指令。 它还将构建“上下文”(在构建过程中应该可用的本地文件系统层次结构)发送到 Docker 守护程序。 通常,当前目录被设置为构建上下文。
访问包含 Dockerfile
的目录后,运行 docker build
,传入带有 -t
标志的图像和标签名称,并使用当前目录作为构建上下文。 在这里,我们将图像命名为 django-polls
并用版本 v0
标记它:
docker build -t django-polls:v0 .
该命令会将 Dockerfile
和当前目录作为构建上下文传递给 Docker 守护进程。 守护进程将通过在处理 Dockerfile
指令时创建一系列图像层来构建您的图像。
当 docker build
完成时,您应该看到以下输出:
OutputSuccessfully built 8260b58f5713 Successfully tagged django-polls:v0
成功构建镜像后,您可以使用 docker run
运行应用程序容器。 但是,run
命令在这里很可能会失败,因为我们还没有配置容器的运行环境。 SECRET_KEY
等外部变量和 settings.py
中的数据库设置将为空白或设置为默认值。
在最后一步,我们将使用环境变量文件配置容器的运行环境。 然后,我们将创建数据库模式,生成应用程序的静态文件并将其上传到对象存储,最后测试应用程序。
第 7 步 - 配置运行环境并测试应用程序
Docker 提供 几种方法 用于设置容器内部的环境变量。 由于我们必须设置在步骤 1 中外部化的所有变量,我们将使用 --env-file
方法,它允许我们传入一个包含环境变量列表及其值的文件。
在 polls-project
目录中创建一个名为 env
的文件,并粘贴以下变量列表:
民意调查项目/环境
DJANGO_SECRET_KEY=your_secret_key DEBUG=True DJANGO_ALLOWED_HOSTS=your_server_IP_address DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME=sammy DATABASE_PASSWORD=your_database_password DATABASE_HOST=your_database_host DATABASE_PORT=your_database_port STATIC_ACCESS_KEY_ID=your_space_access_key_id STATIC_SECRET_KEY=your_space_secret_key STATIC_BUCKET_NAME=your_space_name STATIC_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com DJANGO_LOGLEVEL=info
替换此文件中的以下值:
DJANGO_SECRET_KEY
:将此设置为唯一的、不可预测的值,如 Django 文档 中所述。 Scalable Django App 教程的 Adjusting the App Settings 提供了一种生成此密钥的方法。DJANGO_ALLOWED_HOSTS
:将此设置为您的 Ubuntu 服务器的 IP 地址。 出于测试目的,您还可以将其设置为*
,这是一个匹配所有主机的通配符。 在生产环境中运行 Django 时,请务必正确设置此值。DATABASE_USERNAME
:设置为上一步创建的数据库用户。DATABASE_PASSWORD
:设置为上一步创建的用户密码。DATABASE_HOST
:将此设置为数据库的主机名。DATABASE_PORT
:将此设置为数据库的端口。STATIC_ACCESS_KEY_ID
:将此设置为您空间的访问密钥。STATIC_SECRET_KEY
:将此设置为您空间的访问密钥 Secret。STATIC_BUCKET_NAME
:将此设置为您的空间名称。STATIC_ENDPOINT_URL
:将此设置为适当的空间端点 URL。
在生产环境中运行 Django 时,请务必将 DEBUG
设置为 False
并根据您想要的详细程度调整日志级别。
保存并关闭文件。
我们现在将使用 docker run
覆盖 Dockerfile 中设置的 CMD
并使用 manage.py makemigrations
和 manage.py migrate
命令创建数据库模式:
docker run --env-file env django-polls:v0 sh -c "python manage.py makemigrations && python manage.py migrate"
在这里,我们运行 django-polls:v0
容器镜像,传入我们刚刚创建的环境变量文件,并使用 sh -c "python manage.py makemigrations && python manage.py migrate"
覆盖 Dockerfile 命令,这将创建应用程序代码定义的数据库架构。 运行命令后,您应该看到:
OutputNo changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.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 contenttypes.0002_remove_content_type_name... 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 polls.0001_initial... OK Applying sessions.0001_initial... OK
这表明数据库模式已成功创建。
接下来,我们将运行应用程序容器的另一个实例,并在其中使用交互式 shell 为 Django 项目创建管理用户。
docker run -i -t --env-file env django-polls:v0 sh
这将在正在运行的容器内为您提供一个 shell 提示,您可以使用它来创建 Django 用户:
python manage.py createsuperuser
输入用户的用户名、电子邮件地址和密码,创建用户后,点击 CTRL+D
退出容器并杀死它。
最后,我们将为应用程序生成静态文件并使用 collectstatic
将它们上传到 DigitalOcean Space:
docker run --env-file env django-polls:v0 sh -c "python manage.py collectstatic --noinput"
Output 121 static files copied.
我们现在可以运行应用程序:
docker run --env-file env -p 80:8000 django-polls:v0
Output[2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9
在这里,我们运行 Dockerfile 中定义的默认命令 gunicorn --bind :8000 --workers 3 mysite.wsgi:application
,并暴露容器端口 8000
以便 Ubuntu 服务器上的端口 80
映射到端口 [ django-polls:v0
容器的 X175X]。
您现在应该可以通过在 URL 栏中键入 http://your_server_ip
使用 Web 浏览器导航到 polls
应用程序。 由于没有为 /
路径定义路由,因此您可能会收到 404 Page Not Found 错误,这是预期的。
导航到 http://your_server_ip/polls
以查看投票应用程序界面:
要查看管理界面,请访问 http://your_server_ip/admin
。 您应该会看到 Polls 应用管理员身份验证窗口:
输入您使用 createsuperuser
命令创建的管理用户名和密码。
验证后,您可以访问投票应用的管理界面:
请注意,admin
和 polls
应用程序的静态资产直接从对象存储中交付。 要确认这一点,请参阅 Testing Spaces 静态文件交付 。
完成探索后,在运行 Docker 容器的终端窗口中点击 CTRL-C
以杀死容器。
结论
在本教程中,您调整了 Django Web 应用程序以在基于容器的云原生环境中有效工作。 然后,您为容器映像编写了一个最小的 Dockerfile,在本地构建它,并使用 Docker Engine 运行它。 您可以在 Polls 应用程序 GitHub 存储库的 polls-docker 分支 中看到您实施的更改的 diff。 此分支包含本教程中描述的所有修改。
从这里,您可以将 Django/Gunicorn 容器与 Nginx 反向代理容器配对以处理和路由传入的 HTTP 请求,并将 Certbot 容器配对以获取 TLS 证书。 您可以使用 Docker Compose 管理这种多容器架构; 这将在后续教程中进行描述。
请注意,按原样,此设置是 不是 生产准备就绪,因为您应该 始终 在 HTTP 代理后面运行 Gunicorn 以缓冲慢速客户端。 否则,您的 Django Web 应用程序将容易受到拒绝服务攻击。 在本教程中,我们还选择了 3 作为任意数量的 Gunicorn 工人。 在生产中,您应该使用性能基准设置工作人员和线程的数量。
在这个架构中,我们做出了将静态资产卸载到对象存储的设计选择,这样容器就不必捆绑这些资产的一个版本并使用 Nginx 为它们提供服务,这在 Kubernetes 等多容器集群环境中管理起来会变得很麻烦. 根据您的用例,这可能不是一个有效的设计,因此您应该相应地调整本教程中的步骤。
最后,既然您已经完全容器化了 Django Polls 应用程序,您可以将图像推送到容器注册表,如 Dockerhub,并使其可用于任何 Docker 可用的系统:Ubuntu 服务器、虚拟机和Kubernetes 等容器集群。