如何使用 Docker、Nginx 和 Let’s Encrypt 扩展和保护 Django 应用程序

介绍

在基于云的环境中,有多种方法可以扩展和保护Django应用程序。通过水平扩展并运行应用程序的多个副本,您可以构建一个容错性更强、可用性更高的系统,同时还增加其吞吐量,以便可以同时处理请求。水平缩放一个Django应用程序的一种方法是提供额外的应用服务器运行你的Django应用程序和WSGI HTTP服务器(如GunicornuWSGI)。要在这组应用服务器之间路由和分发传入请求,您可以使用负载均衡器反向代理,Nginx. Nginx 还可以缓存静态内容并终止传输层安全(TLS) 连接,用于为您的应用程序提供 HTTPS 和安全连接。

在 Docker容器内运行 Django 应用程序和 Nginx 代理可确保这些组件的行为方式相同,而不管它们部署到何种环境中。此外,容器提供了许多有助于打包和配置应用程序的功能。

在本教程中,您将通过配置两个应用程序服务器来水平扩展容器化的 Django 和 Gunicorn Polls应用程序,每个应用程序服务器都运行一个 Django 和 Gunicorn 应用程序容器的副本。

您还将通过配和配置将运行 Nginx 反向代理容器和Certbot客户端容器的第三个代理服务器来启用 HTTPS Certbot 将从Let’s Encrypt证书颁发机构为 Nginx 提供 TLS 证书这将确保您的站点获得SSL Labs的高安全评级该代理服务器将接收您应用程序的所有外部请求,并位于两个上游Django 应用程序服务器的前面最后,您将通过限制对代理服务器的外部访问来强化这个分布式系统。

先决条件

要学习本教程,您需要:

  • 三台 Ubuntu 18.04 服务器:

    • 两台服务器将是应用程序服务器,用于运行您的 Django 和 Gunicorn 应用程序。
    • 一台服务器将作为代理服务器,用于运行 Nginx 和 Certbot。
    • 所有人都应该有一个具有sudo特权的非 root 用户和一个活动的防火墙。有关如何设置这些的指导,请参阅此初始服务器设置指南
  • Docker 安装在所有三台服务器上。有关安装 Docker 的指导,请按照如何在 Ubuntu 18.04 上安装和使用 Docker 的步骤 1 和 2 进行操作

  • 一个注册的域名。本教程将your_domain.com贯穿始终。您可以在Freenom免费获得一个,或者使用您选择的域名注册商。

  • 指向代理服务器公共 IP 地址ADNS 记录您可以按照DigitalOcean DNS的介绍了解有关如何将其添加到 DigitalOcean 帐户的详细信息,如果这是您使用的。your_domain.com

  • 一个 S3 对象存储桶,例如DigitalOcean Space,用于存储 Django 项目的静态文件和此空间的一组访问密钥。要了解如何创建空间,请参阅如何创建空间产品文档。要了解如何为空间创建访问密钥,请参阅使用访问密钥共享对空间的访问只需稍作更改,您就可以使用django-storages插件支持的任何对象存储服务

  • Django 应用程序的 PostgreSQL 服务器实例、数据库和用户。只需稍作更改,您就可以使用Django 支持的任何数据库

步骤 1 — 配置第一个 Django 应用服务器

首先,我们将 Django 应用程序存储库克隆到第一个应用程序服务器上。然后,我们将配置和构建应用程序 Docker 映像,并通过运行 Django 容器来测试应用程序。

注意:如果您继续学习如何使用 Docker 构建 Django 和 Gunicorn 应用程序,您将已经完成第 1 步,可以跳到第 2 步来配置第二个应用程序服务器。

首先登录到两个 Django 应用程序服务器中的第一个,并使用git克隆polls-dockerDjango Tutorial Polls App GitHub 存储库分支此 repo 包含 Django 文档的示例 Polls 应用程序的代码polls-docker分支包含 Polls 应用程序的 Dockerized 版本。要了解如何修改 Polls 应用程序以在容器化环境中有效工作,请参阅如何使用 Docker 构建 Django 和 Gunicorn 应用程序

  • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

导航到django-polls目录:

cd django-polls

该目录包含 Django 应用程序 Python 代码,DockerfileDocker 将使用该代码构建容器映像,以及一个env包含要传递到容器运行环境的环境变量列表文件。检查Dockerfile使用cat

cat Dockerfile
Output
FROM python:3.7.4-alpine3.10 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 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"]

此 Dockerfile 使用官方 Python 3.7.4 Docker 映像作为基础,并安装django-polls/requirements.txt文件中定义的 Django 和 Gunicorn 的 Python 包要求然后删除一些不必要的构建文件,将应用程序代码复制到映像中,并设置执行PATH最后,它声明 port8000将用于接受传入的容器连接,并gunicorn与 3 个工作人员一起运行,侦听 port 8000

要了解有关此 Dockerfile 中每个步骤的更多信息,请参阅如何使用 Docker 构建 Django 和 Gunicorn 应用程序的步骤 6

现在,使用docker build以下命令构建图像

  • docker build -t polls .

我们polls使用-t标志命名图像并将当前目录作为构建上下文传递,即构建图像时要引用的文件集。

在 Docker 构建并标记镜像后,使用docker images以下命令列出可用镜像

docker images

您应该会看到polls列出图像:

Output
REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB

在运行 Django 容器之前,我们需要使用env当前目录中存在文件配置其运行环境这个文件会传入docker run用于运行容器命令中,Docker会将配置好的环境变量注入到容器的运行环境中。

env使用nano或您喜欢的编辑器打开文件

nano env

我们将像这样配置文件,您需要添加一些附加值,如下所述。

django-民意调查/环境
DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=
DATABASE_ENGINE=postgresql_psycopg2
DATABASE_NAME=polls
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
STATIC_ACCESS_KEY_ID=
STATIC_SECRET_KEY=
STATIC_BUCKET_NAME=
STATIC_ENDPOINT_URL=
DJANGO_LOGLEVEL=info

填写以下键的缺失值:

  • DJANGO_SECRET_KEY:将其设置为唯一的、不可预测的值,如Django 文档中所述调整可扩展 Django 应用教程的应用设置提供了一种生成此密钥的方法
  • DJANGO_ALLOWED_HOSTS:此变量保护应用程序并防止 HTTP 主机标头攻击。出于测试目的,将此设置为*,将匹配所有主机的通配符。在生产中,您应该将其设置为your_domain.com. 要了解有关此 Django 设置的更多信息,请参阅Django 文档中的核心设置
  • DATABASE_USERNAME:将此设置为在先决条件步骤中创建的 PostgreSQL 数据库用户。
  • DATABASE_NAME:将此设置为polls或在先决条件步骤中创建的 PostgreSQL 数据库的名称。
  • DATABASE_PASSWORD:将此设置为在先决条件步骤中创建的 PostgreSQL 用户密码。
  • DATABASE_HOST:将其设置为数据库的主机名。
  • DATABASE_PORT:将其设置为您的数据库端口。
  • STATIC_ACCESS_KEY_ID:将此设置为您的 S3 存储桶或空间的访问密钥。
  • STATIC_SECRET_KEY:将此设置为您的 S3 存储桶或 Space 的访问密钥 Secret。
  • STATIC_BUCKET_NAME:将此设置为您的 S3 存储桶或空间名称。
  • STATIC_ENDPOINT_URL:将此设置为适当的 S3 存储桶或空间端点 URL,例如,如果您的空间位于该区域。https://space-name.nyc3.digitaloceanspaces.comnyc3

完成编辑后,保存并关闭文件。

我们现在将docker run用于覆盖CMDDockerfile 中的集合并使用manage.py makemigrationsmanage.py migrate命令创建数据库模式

docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"

我们运行polls:latest容器镜像,传入我们刚刚修改的环境变量文件,并用 覆盖 Dockerfile 命令sh -c "python manage.py makemigrations && python manage.py migrate",这将创建由应用程序代码定义的数据库架构。如果您是第一次运行它,您应该看到:

Output
No 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

这表明已成功创建数据库模式。

如果您在migrate后续运行,除非数据库架构已更改,否则 Django 将执行空操作。

接下来,我们将运行应用程序容器的另一个实例,并在其中使用交互式 shell 为 Django 项目创建管理用户。

docker run -i -t --env-file env polls sh

这将在正在运行的容器内为您提供一个 shell 提示,您可以使用它来创建 Django 用户:

python manage.py createsuperuser

为您的用户输入用户名、电子邮件地址和密码,创建用户后,点击CTRL+D退出容器并杀死它。

最后,我们将为应用程序生成静态文件并使用collectstatic. 请注意,这可能需要一些时间才能完成。

docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"

生成并上传这些文件后,您将收到以下输出。

Output
121 static files copied.

我们现在可以运行应用程序:

docker run --env-file env -p 80:8000 polls
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,使港口80Ubuntu的服务器上被映射到端口8000的的polls容器。

您现在应该可以polls通过在 URL 栏中键入来使用 Web 浏览器导航到该应用程序由于没有为路径定义路由,您可能会收到一个错误,这是意料之中的。http://APP_SERVER_1_IP/404 Page Not Found

警告:将 UFW 防火墙与 Docker 一起使用时,Docker 会绕过任何已配置的 UFW 防火墙规则,如此GitHub 问题 中所述这解释了为什么您可以访问80服务器端口,即使您没有在任何先决条件步骤中明确创建 UFW 访问规则。第 5 步中,我们将通过修补 UFW 配置来解决此安全漏洞。如果您没有使用 UFW 而是使用 DigitalOcean 的云防火墙,则可以放心地忽略此警告。

导航到Polls 应用界面:http://APP_SERVER_1_IP/polls

投票应用界面

要查看管理界面,请访问您应该会看到 Polls 应用程序管理员身份验证窗口:http://APP_SERVER_1_IP/admin

投票管理员身份验证页面

输入您使用该createsuperuser命令创建的管理用户名和密码

身份验证后,您可以访问 Polls 应用程序的管理界面:

投票管理主界面

请注意,应用程序adminpolls应用程序的静态资产直接从对象存储交付。要确认这一点,请参阅测试空间静态文件交付

完成探索后,CTRL+C在运行 Docker 容器的终端窗口中点击以终止容器。

现在您已经确认应用程序容器按预期运行,您可以在分离模式下运行它,这将在后台运行并允许您注销 SSH 会话:

docker run -d --rm --name polls --env-file env -p 80:8000 polls

-d标志指示 Docker 以分离模式运行容器,该-rm标志在容器退出后清理容器的文件系统,我们将容器命名为polls.

注销第一个 Django 应用服务器,然后导航到以确认容器按预期运行。http://APP_SERVER_1_IP/polls

现在您的第一个 Django 应用服务器已启动并运行,您可以设置第二个 Django 应用服务器。

步骤 2 — 配置第二个 Django 应用服务器

由于设置此服务器的许多命令将与上一步中的命令相同,因此将在此处以缩写形式呈现。有关此步骤中任何特定命令的更多信息,请查看步骤 1

首先登录到第二个Django 应用程序服务器。

克隆GitHub 存储库polls-docker分支django-polls

  • git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git

导航到django-polls目录:

cd django-polls

使用docker build以下方法构建图像

  • docker build -t polls .

env使用nano或您喜欢的编辑器打开文件

nano env
django-民意调查/环境
DJANGO_SECRET_KEY=
DEBUG=True
DJANGO_ALLOWED_HOSTS=
DATABASE_ENGINE=postgresql_psycopg2
DATABASE_NAME=polls
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_HOST=
DATABASE_PORT=
STATIC_ACCESS_KEY_ID=
STATIC_SECRET_KEY=
STATIC_BUCKET_NAME=
STATIC_ENDPOINT_URL=
DJANGO_LOGLEVEL=info

按照步骤 1填写缺失值完成编辑后,保存并关闭文件。

最后,以分离模式运行应用程序容器:

docker run -d --rm --name polls --env-file env -p 80:8000 polls

导航到以确认容器按预期运行。您可以安全地注销第二个应用程序服务器,而无需终止正在运行的容器。http://APP_SERVER_2_IP/polls

随着 Django 应用容器的启动和运行,您可以继续配置 Nginx 反向代理容器。

第 3 步 – 配置 Nginx Docker 容器

Nginx是一个多功能的 Web 服务器,它提供了许多功能,包括反向代理负载平衡缓存在本教程中,我们已将 Django 的静态资产卸载到对象存储,因此我们不会使用 Nginx 的缓存功能。但是,我们将使用 Nginx 作为我们两个后端 Django 应用服务器的反向代理,并在它们之间分发传入的请求。此外,Nginx 将使用 Certbot 提供的 TLS 证书执行TLS 终止和重定向。这意味着它将强制客户端使用 HTTPS,将传入的 HTTP 请求重定向到端口 443。然后它将解密 HTTPS 请求并将它们代理到上游 Django 服务器。

在本教程中,我们做出了将 Nginx 容器与后端服务器分离的设计决策。根据您的用例,您可以选择在其中一个 Django 应用服务器上运行 Nginx 容器,在本地代理请求以及到另一个 Django 服务器。另一种可能的架构是运行两个 Nginx 容器,每个后端服务器一个,前面有一个云负载均衡器每种架构都呈现出不同的安全和性能优势,您应该进行负载测试您的系统来发现瓶颈。本教程中描述的灵活架构允许您扩展后端 Django 应用程序层以及 Nginx 代理层。一旦单个 Nginx 容器成为瓶颈,您可以横向扩展到多个 Nginx 代理,并添加云负载均衡器或快速 L4 负载均衡器(如HAProxy )

随着 Django 应用服务器的启动和运行,我们可以开始设置 Nginx 代理服务器。登录到您的代理服务器并创建一个名为 的目录conf

mkdir conf

创建一个名为nginx.confusingnano或您喜欢的编辑器的配置文件

nano conf/nginx.conf

粘贴以下 Nginx 配置:

配置文件/nginx.conf

upstream django {
    server APP_SERVER_1_IP;
    server APP_SERVER_2_IP;
}

server {
    listen 80 default_server;
    return 444;
}

server {
    listen 80;
    listen [::]:80;
    server_name your_domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your_domain.com;

    # SSL
    ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;

    ssl_session_cache shared:le_nginx_SSL:10m;
    ssl_session_timeout 1440m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

    client_max_body_size 4G;
    keepalive_timeout 5;

        location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;
          proxy_pass http://django;
        }

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
    }

}

这些upstream,serverlocation块将 Nginx 配置为将 HTTP 请求重定向到 HTTPS,并在步骤 1 和 2 中配置的两个 Django 应用服务器之间对它们进行负载均衡。要了解有关 Nginx 配置文件结构的更多信息,请参阅有关理解 Nginx 配置的文章文件结构和配置上下文此外,这篇关于理解 Nginx 服务器和位置块选择算法的文章可能会有所帮助。

这个配置在从提供示例配置文件组装GunicornCerbotNginx的,并意味着作为最小的Nginx配置得到这个架构和运行。调整此 Nginx 配置超出了本文的范围,但您可以使用NGINXConfig 之类的工具为您的架构生成高性能且安全的 Nginx 配置文件。

upstream块定义了用于将请求代理到使用proxy_pass指令的服务器组

配置文件/nginx.conf
upstream django {
    server APP_SERVER_1_IP;
    server APP_SERVER_2_IP;
}
. . .

在此块中,我们命名上游django并包含两个 Django 应用服务器的 IP 地址。如果应用服务器在 DigitalOcean 上运行并启用了 VPC 网络,您应该在此处使用它们的私有 IP 地址。要了解如何在 DigitalOcean 上启用 VPC 网络,请参阅如何在现有 Droplet 上启用 VPC 网络

第一个server块捕获与您的域不匹配的请求并终止连接。例如,对服务器 IP 地址的直接 HTTP 请求将由以下块处理:

配置文件/nginx.conf
. . .
server {
    listen 80 default_server;
    return 444;
}
. . .

下一个server块使用HTTP 301 重定向将 HTTP 请求重定向到您的域到 HTTPS 然后这些请求由最后一个server处理

配置文件/nginx.conf
. . .
server {
    listen 80;
    listen [::]:80;
    server_name your_domain.com;
    return 301 https://$server_name$request_uri;
}
. . .

这两个指令定义了 TLS 证书和密钥的路径。这些将使用 Certbot 进行配置,并在下一步中安装到 Nginx 容器中。

配置文件/nginx.conf
. . .
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
. . .

这些参数是 Certbot 推荐的 SSL 安全默认值。要了解有关它们的更多信息,请参阅Nginx 文档中的模块 ngx_http_ssl_moduleMozilla 的安全/服务器端 TLS是另一个有用的指南,您可以使用它来调整 SSL 配置。

配置文件/nginx.conf
. . .
    ssl_session_cache shared:le_nginx_SSL:10m;
    ssl_session_timeout 1440m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
. . .

Gunicorn 的示例 Nginx 配置中的这两个指令设置了客户端请求正文的最大允许大小,并为与客户端的保持活动连接分配超时。Nginx 将在keepalive_timeout几秒钟后关闭与客户端的连接

配置文件/nginx.conf
. . .
client_max_body_size 4G;
keepalive_timeout 5;
. . .

第一个location块指示 Nginxupstream django通过 HTTP将请求代理到服务器。它还保留捕获原始 IP 地址、用于连接的协议和目标主机的客户端 HTTP 标头:

配置文件/nginx.conf
. . .
location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://django;
}
. . .

要了解有关这些指令的更多信息,请参阅Nginx 文档中的部署 Gunicorn模块 ngx_http_proxy_module

最后一个location块捕获对/well-known/acme-challenge/路径的请求,Certbot 使用它进行 HTTP-01 质询,以通过 Let’s Encrypt 验证您的域并提供或更新 TLS 证书。有关 Certbot 使用的 HTTP-01 质询的更多信息,请参阅Let’s Encrypt 文档中的质询类型

配置文件/nginx.conf
. . .
location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
}

完成编辑后,保存并关闭文件。

您现在可以使用此配置文件来运行 Nginx Docker 容器。在本教程中,我们将使用由 Nginx 维护官方 Docker 镜像nginx:1.19.0镜像版本1.19.0

当我们第一次运行容器时,Nginx 会抛出错误并失败,因为我们还没有提供配置文件中定义的证书。但是,我们仍将运行命令在本地下载 Nginx 映像并测试其他一切是否正常运行:

docker run --rm --name nginx -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

在这里我们命名容器nginx和映射主机端口80,并443以各自的集装箱港口。-v标志将配置文件挂载到 Nginx 容器中/etc/nginx/conf.d/nginx.conf,Nginx 映像已预先配置为加载。它以ro“只读”模式挂载,因此容器无法修改文件。web 根目录/var/www/html也挂载到容器中。最后nginx:1.19.0指示 Dockernginx:1.19.0从 Dockerhub拉取并运行镜像。

Docker 会拉取并运行镜像,然后 Nginx 会在找不到配置的 TLS 证书和密钥时抛出错误。在下一步中,我们将使用 Dockerized Certbot 客户端和 Let’s Encrypt 证书颁发机构提供这些。

第 4 步 – 配置 Certbot 并让我们加密证书更新

Certbot是由电子前沿基金会开发的 Let’s Encrypt 客户端它提供来自Let’s Encrypt证书颁发机构的免费 TLS 证书,允许浏览器验证您的 Web 服务器的身份。鉴于我们在 Nginx 代理服务器上安装了 Docker,我们将使用Certbot Docker 镜像来提供和更新 TLS 证书。

首先确保您将 DNSA记录映射到代理服务器的公共 IP 地址。然后,在您的代理服务器上,使用certbotDocker 映像提供证书的暂存版本

docker run -it --rm -p 80:80 --name certbot \
         -v "/etc/letsencrypt:/etc/letsencrypt" \
         -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
         certbot/certbot certonly --standalone --staging -d your_domain.com

此命令certbot以交互模式运行Docker 镜像,并将80主机上的端口转发到容器端口80它创建两个主机目录并将其挂载到容器中:/etc/letsencrypt//var/lib/letsencrypt/. certbotstandalone没有 Nginx 的模式下运行,并将使用 Let’s Encryptstaging服务器来执行域验证。

出现提示时,输入您的电子邮件地址并同意服务条款。如果域验证成功,您应该看到以下输出:

Output
Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal.

您可以使用cat以下方法检查证书

sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem

配置 TLS 证书后,我们可以测试在上一步中组装的 Nginx 配置:

docker run --rm --name nginx -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

这与步骤 3 中运行的命令相同,添加了两个最近创建的 Let’s Encrypt 目录。

一旦 Nginx 启动并运行,导航到. 您可能会在浏览器中收到证书颁发机构无效的警告。这是意料之中的,因为我们已经提供了临时证书,而不是生产 Let’s Encrypt 证书。检查浏览器的 URL 栏以确认您的 HTTP 请求已重定向到 HTTPS。http://your_domain.com

点击CTRL+C终端退出 Nginx,certbot再次运行客户端,这次省略--staging标志:

docker run -it --rm -p 80:80 --name certbot \
         -v "/etc/letsencrypt:/etc/letsencrypt" \
         -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
         certbot/certbot certonly --standalone -d your_domain.com

当提示保留现有证书或更新和替换它时,点击2更新它,然后ENTER确认您的选择。

配置生产 TLS 证书后,再次运行 Nginx 服务器:

docker run --rm --name nginx -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
    -v /var/www/html:/var/www/html \
    nginx:1.19.0

在浏览器中,导航到在 URL 栏中,确认 HTTP 请求已重定向到 HTTPS。鉴于 Polls 应用程序没有配置默认路由,您应该会看到 Django Page not found错误。导航到您将看到标准的 Polls 应用程序界面:http://your_domain.comhttps://your_domain.com/polls

投票应用界面

此时,您已经使用 Certbot Docker 客户端提供了一个生产 TLS 证书,并且正在对两个 Django 应用服务器的外部请求进行反向代理和负载平衡。

Let’s Encrypt 证书每 90 天到期一次。为确保您的证书保持有效,您应在预定到期前定期更新。在 Nginx 运行的情况下,您应该在webroot模式而不是standalone模式下使用 Certbot 客户端这意味着 Certbot 将通过在/var/www/html/.well-known/acme-challenge/目录中创建一个文件来执行验证,并且对该路径的 Let’s Encrypt 验证请求将被步骤 3 中locationNginx 配置中定义规则捕获然后 Certbot 将轮换证书,您可以重新加载 Nginx 以便它使用这个新配置的证书。

有多种方法可以自动执行此过程,TLS 证书的自动更新超出了本教程的范围。有关使用cron调度实用程序的类似过程,请参阅如何使用 Nginx、Let’s Encrypt 和 Docker Compose 保护容器化 Node.js 应用程序的步骤 6

在您的终端中,点击CTRL+C杀死 Nginx 容器。通过附加-d标志以分离模式再次运行它

docker run --rm --name nginx -d -p 80:80 -p 443:443 \
    -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \
    -v /etc/letsencrypt:/etc/letsencrypt \
    -v /var/lib/letsencrypt:/var/lib/letsencrypt \
  -v /var/www/html:/var/www/html \
    nginx:1.19.0

在后台运行 Nginx 的情况下,使用以下命令执行证书更新过程的试运行:

docker run -it --rm --name certbot \
    -v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  -v "/var/www/html:/var/www/html" \
  certbot/certbot renew --webroot -w /var/www/html --dry-run

我们使用该--webroot插件,指定 Web 根路径,并使用该--dry-run标志来验证一切是否正常工作,而无需实际执行证书更新。

如果更新模拟成功,您应该看到以下输出:

Output
Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

在生产环境中,更新证书后,您应该重新加载 Nginx 以使更改生效。要重新加载 Nginx,请运行以下命令:

docker kill -s HUP nginx

此命令将向Docker 容器内运行的 Nginx 进程发送HUP Unix 信号nginx收到此信号后,Nginx 将重新加载其配置并更新证书。

启用 HTTPS 并启动并运行此架构的所有组件后,最后一步是通过阻止外部访问两个后端应用程序服务器来锁定设置;所有 HTTP 请求都应该流经 Nginx 代理。

第 5 步 – 防止外部访问 Django 应用服务器

在本教程中描述的架构中,SSL 终止发生在 Nginx 代理上。这意味着 Nginx 会解密 SSL 连接,并且数据包被代理到未加密的 Django 应用服务器。对于许多用例,这种安全级别就足够了。对于涉及财务或健康数据的应用程序,您可能希望实施端到端加密。您可以通过负载均衡器转发加密数据包并在应用服务器上解密,或在代理重新加密并再次在 Django 应用服务器上解密来实现此目的。这些技术超出了本文的范围,但要了解更多信息,请参阅端到端加密

Nginx 代理充当外部流量和内部网络之间的网关。理论上,外部客户端不应该直接访问内部应用服务器,所有请求都应该流经 Nginx 服务器。步骤 1 中的注释简要描述了Docker 的一个开放问题,其中 Dockerufw默认绕过防火墙设置并向外部打开端口,这可能不安全。为了解决这个安全问题,建议在使用支持 Docker 的服务器时使用云防火墙要获取有关使用 DigitalOcean 创建云防火墙的更多信息,请参阅如何创建防火墙您也可以iptables直接操作而不是使用ufw. 要了解有关使用的更多信息iptables使用 Docker,请参阅Docker 和 iptables

在这一步中,我们将修改 UFW 的配置以阻止外部访问 Docker 打开的主机端口。在应用服务器上运行 Django 时,我们将-p 80:8000标志传递docker,它将80主机上的端口转发到容器端口8000这也80向外部客户端开放了端口,您可以通过访问. 为了防止直接访问,我们将使用ufw-docker GitHub 存储库中描述的方法修改 UFW 的配置http://your_app_server_1_IP

首先登录到第一个 Django 应用服务器。然后,/etc/ufw/after.rules使用超级用户权限打开文件,使用nano或您喜欢的编辑器:

sudo nano /etc/ufw/after.rules

出现提示时输入您的密码,然后点击ENTER确认。

您应该看到以下ufw规则:

/etc/ufw/after.rules
#
# rules.input-after
#
# Rules that should be run after the ufw command line added rules. Custom
# rules should be added to one of these chains:
#   ufw-after-input
#   ufw-after-output
#   ufw-after-forward
#

# Don't delete these required lines, otherwise there will be errors
*filter
:ufw-after-input - [0:0]
:ufw-after-output - [0:0]
:ufw-after-forward - [0:0]
# End required lines

# don't log noisy services by default
-A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
-A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
-A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input

# don't log noisy broadcast
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input

# don't delete the 'COMMIT' line or these rules won't be processed
COMMIT

滚动到底部,然后粘贴以下 UFW 配置规则块:

/etc/ufw/after.rules
. . .

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

这些规则限制由泊坞窗打开的端口公共接入,并且能够从接入10.0.0.0/8172.16.0.0/12192.168.0.0/16专用IP范围。如果您将 VPC 与 DigitalOcean 一起使用,那么您的 VPC 网络中的 Droplet 将可以通过专用网络接口访问开放端口,但外部客户端则不能。关于 VPC 的更多信息,请参见VPC 官方文档要了解有关此代码段中实现的规则的更多信息,请参阅它是如何工作的?来自ufw-docker README

如果您没有将 VPC 与 DigitalOcean 一起使用,并且在upstream您的 Nginx 配置块中输入了应用程序服务器的公共 IP 地址,则您必须明确修改 UFW 防火墙以允许来自 Nginx 服务器的流量通过80Django 应用程序上的端口服务器。有关allow使用 UFW 防火墙创建规则的指南,请参阅UFW 要点:通用防火墙规则和命令

完成编辑后,保存并关闭文件。

重新启动ufw以获取新配置:

sudo systemctl restart ufw

在 Web 浏览器中导航到以确认您无法再通过端口访问应用服务器http://APP_SERVER_1_IP80

在第二个 Django 应用服务器上重复此过程。

注销第一个应用服务器或打开另一个终端窗口,然后登录到第二个 Django 应用服务器。然后,/etc/ufw/after.rules使用超级用户权限打开文件,使用nano或您喜欢的编辑器:

sudo nano /etc/ufw/after.rules

出现提示时输入您的密码,然后点击ENTER确认。

滚动到底部,然后粘贴以下 UFW 配置规则块:

/etc/ufw/after.rules
. . .

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

完成编辑后,保存并关闭文件。

重新启动ufw以获取新配置:

sudo systemctl restart ufw

在 Web 浏览器中导航到以确认您无法再通过端口访问应用服务器http://APP_SERVER_2_IP80

最后,导航到以确认 Nginx 代理仍然可以访问上游 Django 服务器。您应该会看到默认的 Polls 应用程序界面。https://your_domain_here/polls

结论

在本教程中,您已经使用 Docker 容器设置了一个可扩展的 Django Polls 应用程序。随着流量的增长和系统负载的增加,您可以分别扩展每一层:Nginx 代理层、Django 后端应用程序层和 PostgreSQL 数据库层。

在构建分布式系统时,您通常必须面对多种设计决策,并且有几种架构可能会满足您的用例。本教程中描述的架构旨在作为使用 Django 和 Docker 设计可扩展应用程序的灵活蓝图。

您可能希望在遇到错误时控制容器的行为,或者在系统启动时自动运行容器。为此,您可以使用Systemd 之类的进程管理器或实施重启策略。有关这些的更多信息,请参阅Docker 文档中的自动启动容器

当与运行同一 Docker 映像的多个主机进行大规模合作时,使用AnsibleChef等配置管理工具自动化步骤会更有效要了解有关配置管理的更多信息,请参阅An Introduction to Configuration Management and Automating Server Setup with Ansible: A DigitalOcean Workshop Kit

您还可以使用像Docker Hub这样的镜像注册表来简化部署,而不是在每个主机上构建相同的镜像,它可以集中构建、存储 Docker 镜像并将其分发到多个服务器。与映像注册表一起,持续集成和部署管道可以帮助您构建、测试和部署映像到您的应用程序服务器。有关 CI/CD 的更多信息,请参阅CI/CD 最佳实践简介

觉得文章有用?

点个广告表达一下你的爱意吧 !😁