如何使用Docker、Nginx和Let'sEncrypt扩展和保护Django应用程序
介绍
在基于云的环境中,有多种方法可以扩展和保护 Django 应用程序。 通过横向扩展,并运行你的应用程序的多个副本,你可以构建一个更容错和高可用的系统,同时增加它的吞吐量,以便可以同时处理请求. 水平扩展 Django 应用程序的一种方法是提供额外的 应用程序服务器 来运行您的 Django 应用程序及其 WSGI HTTP 服务器(如 Gunicorn 或 uWSGI)。 要跨这组应用服务器路由和分发传入请求,您可以使用 负载均衡器 和 反向代理 ,如 Nginx。 Nginx 还可以缓存静态内容并终止 传输层安全 (TLS) 连接,用于为您的应用提供 HTTPS 和安全连接。
在 Docker containers 内运行 Django 应用程序和 Nginx 代理可确保这些组件的行为方式相同,无论它们部署到何种环境。 此外,容器提供了许多有助于打包和配置应用程序的功能。
在本教程中,您将通过配置两个应用程序服务器来水平扩展容器化的 Django 和 Gunicorn Polls 应用程序,每个应用程序服务器将运行 Django 和 Gunicorn 应用程序容器的副本。
您还将通过配置和配置第三个代理服务器来启用 HTTPS,该代理服务器将运行 Nginx 反向代理容器和 Certbot 客户端容器。 Certbot 将从 Let's Encrypt 证书颁发机构为 Nginx 提供 TLS 证书。 这将确保您的站点从 SSL Labs 获得高安全等级。 此代理服务器将接收您应用程序的所有外部请求,并位于两个 upstream Django 应用程序服务器的前面。 最后,您将通过将外部访问限制为仅代理服务器来 强化 这个分布式系统。
先决条件
要遵循本教程,您将需要:
- 三台 Ubuntu 18.04 服务器:
- 两台服务器将是 application 服务器,用于运行您的 Django 和 Gunicorn 应用程序。
- 一台服务器将是 proxy 服务器,用于运行 Nginx 和 Certbot。
- 所有人都应该有一个具有
sudo
权限的非 root 用户和一个活动的防火墙。 有关如何设置这些的指导,请参阅此 初始服务器设置指南 。
- Docker 安装在所有三台服务器上。 有关安装 Docker 的指导,请遵循 如何在 Ubuntu 18.04 上安装和使用 Docker 的步骤 1 和 2。
- 一个注册的域名。 本教程将自始至终使用
your_domain.com
。 您可以在 Freenom 免费获得一个,或使用您选择的域名注册商。 A
DNS 记录,其中your_domain.com
指向您的 proxy 服务器的公共 IP 地址。 您可以按照 this Introduction to DigitalOcean DNS 了解如何将其添加到 DigitalOcean 帐户(如果您正在使用)的详细信息。- 一个 S3 对象存储桶,例如 DigitalOcean Space,用于存储您的 Django 项目的静态文件和该空间的一组访问密钥。 要了解如何创建空间,请参阅 如何创建空间 产品文档。 要了解如何为空间创建访问密钥,请参阅 使用访问密钥共享对空间的访问。 稍作改动,您就可以使用 django-storages 插件支持的任何对象存储服务。
- 您的 Django 应用程序的 PostgreSQL 服务器实例、数据库和用户。 只需稍作更改,您就可以使用 Django 支持的任何数据库 。
- PostgreSQL 数据库应称为 polls(或在下面的配置文件中输入的另一个容易记住的名称),在本教程中,数据库用户将命名为 sammy。 有关创建这些的指导,请遵循 如何使用 Docker 构建 Django 和 Gunicorn 应用程序的第 1 步。 您可以从三台服务器中的任何一台执行这些步骤。
- 本教程使用了 DigitalOcean Managed PostgreSQL cluster。 要了解如何创建集群,请参阅 DigitalOcean Managed Databases 产品文档。
- 您还可以安装和运行您自己的 PostgreSQL 实例。 有关在 Ubuntu 服务器上安装和管理 PostgreSQL 的指导,请参阅 如何在 Ubuntu 18.04 上安装和使用 PostgreSQL。
第 1 步——配置第一个 Django 应用服务器
首先,我们将 Django 应用程序存储库克隆到第一个应用服务器上。 然后,我们将配置和构建应用程序 Docker 映像,并通过运行 Django 容器来测试应用程序。
注意:如果您从如何使用Docker构建Django和Gunicorn应用程序继续,您已经完成了第1步,可以跳到第2步[X181X ] 配置 second 应用服务器。
首先登录到两个 Django 应用程序服务器中的第一个,然后使用 git
克隆 Django 教程投票应用程序 GitHub 存储库 的 polls-docker
分支。 此 repo 包含 Django 文档的 示例投票应用程序 的代码。 polls-docker
分支包含一个 Dockerized 版本的 Polls 应用程序。 要了解如何修改 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 代码、Docker 将用于构建容器映像的 Dockerfile
以及包含要传递到容器的环境变量列表的 env
文件运行环境。 使用 cat
检查 Dockerfile
:
cat Dockerfile
OutputFROM 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
。 最后,它声明端口 8000
将用于接受传入的容器连接,并与 3 个工作人员一起运行 gunicorn
,监听端口 8000
。
要了解有关此 Dockerfile 中每个步骤的更多信息,请参阅 如何使用 Docker 构建 Django 和 Gunicorn 应用程序 的第 6 步。
现在,使用 docker build
构建映像:
docker build -t polls .
我们使用 -t
标志将映像命名为 polls
,并将当前目录作为 构建上下文 传递,这是构建映像时要引用的文件集。
在 Docker 构建并标记镜像后,使用 docker images
列出可用的镜像:
docker images
您应该会看到列出的 polls
图像:
OutputREPOSITORY 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会将配置好的环境变量注入到容器的运行环境中。
使用 nano
或您喜欢的编辑器打开 env
文件:
nano env
我们将像这样配置文件,您需要添加一些附加值,如下所述。
django-polls/env
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 文档 中所述。 Scalable Django App 教程的 Adjusting the App Settings 提供了一种生成此密钥的方法。DJANGO_ALLOWED_HOSTS
:此变量保护应用程序并防止 HTTP 主机标头攻击。 出于测试目的,将其设置为*
,这是一个匹配所有主机的通配符。 在生产中,您应该将其设置为your_domain.com
。 要了解有关此 Django 设置的更多信息,请参阅 Django 文档中的 Core Settings。DATABASE_USERNAME
:将此设置为在先决条件步骤中创建的 PostgreSQL 数据库用户。DATABASE_NAME
:将此设置为polls
或在先决条件步骤中创建的 PostgreSQL 数据库的名称。DATABASE_PASSWORD
:将此设置为在先决条件步骤中创建的 PostgreSQL 用户密码。DATABASE_HOST
:将此设置为数据库的主机名。DATABASE_PORT
:将此设置为数据库的端口。STATIC_ACCESS_KEY_ID
:将此设置为您的 S3 存储桶或 Space 的访问密钥。STATIC_SECRET_KEY
:将此设置为您的 S3 存储桶或空间的访问密钥 Secret。STATIC_BUCKET_NAME
:将此设置为您的 S3 存储桶或空间名称。STATIC_ENDPOINT_URL
:将此设置为适当的 S3 存储桶或空间端点 URL,例如https://space-name.nyc3.digitaloceanspaces.com
如果您的空间位于nyc3
区域。
完成编辑后,保存并关闭文件。
我们现在将使用 docker run
覆盖 Dockerfile 中设置的 CMD
并使用 manage.py makemigrations
和 manage.py migrate
命令创建数据库模式:
docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate"
我们运行 polls:latest
容器镜像,传入我们刚刚修改的环境变量文件,并用 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
这表明数据库模式已成功创建。
如果您随后运行 migrate
,除非数据库模式已更改,否则 Django 将执行无操作。
接下来,我们将运行应用程序容器的另一个实例,并在其中使用交互式 shell 为 Django 项目创建管理用户。
docker run -i -t --env-file env polls sh
这将在正在运行的容器内为您提供一个 shell 提示,您可以使用它来创建 Django 用户:
python manage.py createsuperuser
输入用户的用户名、电子邮件地址和密码,创建用户后,点击 CTRL+D
退出容器并杀死它。
最后,我们将为应用程序生成静态文件并使用 collectstatic
将它们上传到 DigitalOcean Space。 请注意,这可能需要一些时间才能完成。
docker run --env-file env polls sh -c "python manage.py collectstatic --noinput"
生成并上传这些文件后,您将收到以下输出。
Output121 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
以便 Ubuntu 服务器上的端口 80
映射到端口 [ polls
容器的 X175X]。
您现在应该可以通过在 URL 栏中键入 http://APP_SERVER_1_IP
使用 Web 浏览器导航到 polls
应用程序。 由于没有为 /
路径定义路由,因此您可能会收到 404 Page Not Found
错误,这是预期的。
警告: 将 UFW 防火墙与 Docker 一起使用时,Docker 会绕过任何已配置的 UFW 防火墙规则,如 GitHub 问题 中所述。 这解释了为什么您可以访问服务器的端口 80
,即使您没有在任何先决条件步骤中明确创建 UFW 访问规则。 在 Step 5 中,我们将通过修补 UFW 配置来解决这个安全漏洞。 如果您没有使用 UFW 而是使用 DigitalOcean 的 Cloud Firewalls,您可以放心地忽略此警告。
导航到 http://APP_SERVER_1_IP/polls
以查看投票应用程序界面:
要查看管理界面,请访问 http://APP_SERVER_1_IP/admin
。 您应该会看到 Polls 应用管理员身份验证窗口:
输入您使用 createsuperuser
命令创建的管理用户名和密码。
验证后,您可以访问投票应用的管理界面:
请注意,admin
和 polls
应用程序的静态资产直接从对象存储中交付。 要确认这一点,请参阅 Testing Spaces 静态文件交付 。
完成探索后,在运行 Docker 容器的终端窗口中点击 CTRL+C
以杀死容器。
现在您已确认应用程序容器按预期运行,您可以在 分离 模式下运行它,这将在后台运行它并允许您注销 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 应用程序服务器
由于设置此服务器的许多命令与上一步中的命令相同,因此在此以缩写形式介绍它们。 有关此步骤中任何特定命令的更多信息,请查看 Step 1。
首先登录到 second Django 应用程序服务器。
克隆 django-polls
GitHub 存储库的 polls-docker
分支:
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 .
使用 nano
或您喜欢的编辑器打开 env
文件:
nano env
django-polls/env
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
如Step 1中填写缺失值。 完成编辑后,保存并关闭文件。
最后,以分离模式运行应用容器:
docker run -d --rm --name polls --env-file env -p 80:8000 polls
导航到 http://APP_SERVER_2_IP/polls
以确认容器按预期运行。 您可以安全地注销第二个应用服务器,而无需终止正在运行的容器。
两个 Django 应用程序容器都启动并运行后,您可以继续配置 Nginx 反向代理容器。
第三步——配置 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
使用 nano
或您喜欢的编辑器创建一个名为 nginx.conf
的配置文件:
nano conf/nginx.conf
粘贴以下 Nginx 配置:
conf/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
、server
和 location
块将 Nginx 配置为将 HTTP 请求重定向到 HTTPS,并在步骤 1 和 2 中配置的两个 Django 应用服务器之间进行负载平衡。 想了解更多关于 Nginx 配置文件结构的知识,请参考这篇文章了解 Nginx 配置文件结构和配置上下文。 此外,这篇关于 Understanding Nginx Server and Location Block Selection Algorithms 的文章可能会有所帮助。
此配置由 Gunicorn、Cerbot 和 Nginx 提供的示例配置文件组装而成,旨在作为最小的 Nginx 配置来启动和运行此架构。 调整此 Nginx 配置超出了本文的范围,但您可以使用 NGINXConfig 之类的工具为您的架构生成高性能且安全的 Nginx 配置文件。
upstream
块定义了用于代理请求以使用 proxy_pass
指令的服务器组:
conf/nginx.conf
upstream django { server APP_SERVER_1_IP; server APP_SERVER_2_IP; } . . .
在这个块中,我们将上游命名为 django
并包括两个 Django 应用服务器的 IP 地址。 如果应用服务器在 DigitalOcean 上运行并启用了 VPC 网络,您应该在此处使用它们的私有 IP 地址。 要了解如何在 DigitalOcean 上启用 VPC 网络,请参阅 如何在现有 Droplets 上启用 VPC 网络。
第一个 server
块捕获与您的域不匹配的请求并终止连接。 例如,对服务器 IP 地址的直接 HTTP 请求将由以下块处理:
conf/nginx.conf
. . . server { listen 80 default_server; return 444; } . . .
下一个 server
块使用 HTTP 301 重定向 将 HTTP 请求重定向到您的域到 HTTPS。 这些请求然后由最终的 server
块处理:
conf/nginx.conf
. . . server { listen 80; listen [::]:80; server_name your_domain.com; return 301 https://$server_name$request_uri; } . . .
这两个指令定义了 TLS 证书和密钥的路径。 这些将使用 Certbot 进行配置,并在下一步中安装到 Nginx 容器中。
conf/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 文档中的 Module ngx_http_ssl_module。 Mozilla 的 Security/Server Side TLS 是另一个有用的指南,可用于调整 SSL 配置。
conf/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
秒后关闭与客户端的连接。
conf/nginx.conf
. . . client_max_body_size 4G; keepalive_timeout 5; . . .
第一个 location
块指示 Nginx 通过 HTTP 将请求代理到 upstream django
服务器。 它还保留了捕获原始 IP 地址、用于连接的协议和目标主机的客户端 HTTP 标头:
conf/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 文档中的 Deploying Gunicorn 和 Module ngx_http_proxy_module。
最后的 location
块捕获对 /well-known/acme-challenge/
路径的请求,Certbot 用于 HTTP-01 挑战,以使用 Let's Encrypt 验证您的域并提供或更新 TLS 证书。 有关 Certbot 使用的 HTTP-01 质询的更多信息,请参阅 Let's Encrypt 文档中的 Challenge Types。
conf/nginx.conf
. . . location ^~ /.well-known/acme-challenge/ { root /var/www/html; }
完成编辑后,保存并关闭文件。
您现在可以使用此配置文件来运行 Nginx Docker 容器。 在本教程中,我们将使用由 Nginx 维护的 official Docker image 的 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
标志将配置文件挂载到位于 /etc/nginx/conf.d/nginx.conf
的 Nginx 容器中,Nginx 映像已预先配置为加载该容器。 它以 ro
或“只读”模式挂载,因此容器无法修改文件。 Web 根目录 /var/www/html
也被挂载到容器中。 最后 nginx:1.19.0
指示 Docker 从 Dockerhub 拉取并运行 nginx:1.19.0
映像。
Docker 会拉取并运行镜像,然后 Nginx 会在找不到配置的 TLS 证书和密钥时抛出错误。 在下一步中,我们将使用 Dockerized Certbot 客户端和 Let's Encrypt 证书颁发机构来配置这些。
第 4 步 — 配置 Certbot 并让我们加密证书更新
Certbot 是由 Electronic Frontier Foundation 开发的 Let's Encrypt 客户端。 它提供来自 Let's Encrypt 证书颁发机构的免费 TLS 证书,允许浏览器验证您的 Web 服务器的身份。 鉴于我们在 Nginx 代理服务器上安装了 Docker,我们将使用 Certbot Docker 映像 来配置和更新 TLS 证书。
首先确保您将 DNS A
记录映射到代理服务器的公共 IP 地址。 然后,在您的代理服务器上,使用 certbot
Docker 映像配置证书的暂存版本:
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/
。 certbot
在 standalone
模式下运行,没有 Nginx,将使用 Let's Encrypt staging
服务器执行域验证。
出现提示时,输入您的电子邮件地址并同意服务条款。 如果域验证成功,您应该看到以下输出:
OutputObtaining 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
这是在 Step 3 中运行的相同命令,并添加了最近创建的两个 Let's Encrypt 目录。
一旦 Nginx 启动并运行,导航到 http://your_domain.com
。 您可能会在浏览器中收到证书颁发机构无效的警告。 这是预期的,因为我们提供了暂存证书而不是生产 Let's Encrypt 证书。 检查浏览器的 URL 栏以确认您的 HTTP 请求已重定向到 HTTPS。
在终端中点击 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
在您的浏览器中,导航到 http://your_domain.com
。 在 URL 栏中,确认 HTTP 请求已重定向到 HTTPS。 鉴于 Polls 应用程序没有配置默认路由,您应该会看到 Django Page not found 错误。 导航到 https://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 验证请求将被 [X220X 中 Nginx 配置中定义的 location
规则捕获]步骤 3。 然后 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
标志来验证一切是否正常工作,而无需实际执行证书更新。
如果更新模拟成功,您应该会看到以下输出:
OutputCert 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
此命令将向 nginx
Docker 容器内运行的 Nginx 进程发送 HUP Unix 信号。 收到此信号后,Nginx 将重新加载其配置并更新证书。
启用 HTTPS 并启动并运行此架构的所有组件后,最后一步是通过阻止外部访问两个后端应用程序服务器来锁定设置; 所有 HTTP 请求都应该通过 Nginx 代理。
第 5 步 - 防止外部访问 Django 应用服务器
在本教程中描述的架构中,SSL 终止发生在 Nginx 代理上。 这意味着 Nginx 解密 SSL 连接,并且数据包被代理到未加密的 Django 应用服务器。 对于许多用例,这种安全级别就足够了。 对于涉及财务或健康数据的应用程序,您可能希望实施端到端加密。 您可以通过负载均衡器转发加密的数据包并在应用服务器上解密,或者在代理上重新加密并再次在 Django 应用服务器上解密来做到这一点。 这些技术超出了本文的范围,但要了解更多信息,请参阅端到端加密。
Nginx 代理充当外部流量和内部网络之间的网关。 理论上,外部客户端不应该直接访问内部应用服务器,所有请求都应该通过 Nginx 服务器。 Step 1 中的注释简要描述了 Docker 的 open issue,其中 Docker 默认绕过 ufw
防火墙设置并在外部打开端口,这可能是不安全的。 为了解决这个安全问题,建议在使用支持 Docker 的服务器时使用 云防火墙 。 要获取有关使用 DigitalOcean 创建云防火墙的更多信息,请参阅 如何创建防火墙 。 您也可以直接操作 iptables
而不是使用 ufw
。 要了解更多关于在 Docker 中使用 iptables
的信息,请参阅 Docker 和 iptables。
在这一步中,我们将修改 UFW 的配置以阻止对 Docker 打开的主机端口的外部访问。 在应用服务器上运行 Django 时,我们将 -p 80:8000
标志传递给 docker
,它将主机上的端口 80
转发到容器端口 8000
。 这也向外部客户端开放了端口80
,您可以通过访问http://your_app_server_1_IP
来验证。 为了防止直接访问,我们将使用 ufw-docker GitHub 存储库 中描述的方法修改 UFW 的配置。
首先登录到第一个 Django 应用服务器。 然后,使用 nano
或您喜欢的编辑器以超级用户权限打开 /etc/ufw/after.rules
文件:
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
这些规则限制对 Docker 打开的端口的公共访问,并允许从 10.0.0.0/8
、172.16.0.0/12
和 192.168.0.0/16
私有 IP 范围进行访问。 如果您将 VPC 与 DigitalOcean 一起使用,则您的 VPC 网络中的 Droplets 将可以通过专用网络接口访问开放端口,但外部客户端则不能。 关于VPC的更多信息,请参见VPC官方文档。 要了解有关此片段中实施的规则的更多信息,请参阅 ufw-docker README 中的 How it works?。
如果您没有将 VPC 与 DigitalOcean 一起使用,并且在 Nginx 配置的 upstream
块中输入了应用服务器的公共 IP 地址,则必须显式修改 UFW 防火墙以允许来自 Nginx 服务器的流量通过 Django 应用服务器上的端口 80
。 有关使用 UFW 防火墙创建 allow
规则的指南,请参阅 UFW Essentials: Common Firewall Rules and Commands。
完成编辑后,保存并关闭文件。
重新启动 ufw
以获取新配置:
sudo systemctl restart ufw
在 Web 浏览器中导航到 http://APP_SERVER_1_IP
以确认您无法再通过端口 80
访问应用服务器。
在第二个 Django 应用服务器上重复此过程。
注销第一个应用服务器或打开另一个终端窗口,然后登录到第二个 Django 应用服务器。 然后,使用 nano
或您喜欢的编辑器以超级用户权限打开 /etc/ufw/after.rules
文件:
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_IP
以确认您无法再通过端口 80
访问应用服务器。
最后,导航到 https://your_domain_here/polls
以确认 Nginx 代理仍然可以访问上游 Django 服务器。 您应该会看到默认的 Polls 应用程序界面。
结论
在本教程中,您已经使用 Docker 容器设置了一个可扩展的 Django Polls 应用程序。 随着流量的增长和系统负载的增加,您可以单独扩展每一层:Nginx 代理层、Django 后端应用程序层和 PostgreSQL 数据库层。
在构建分布式系统时,您通常必须面对多个设计决策,并且多个架构可能会满足您的用例。 本教程中描述的架构旨在作为使用 Django 和 Docker 设计可扩展应用程序的灵活蓝图。
您可能希望在遇到错误时控制容器的行为,或者在系统启动时自动运行容器。 为此,您可以使用 Systemd 之类的进程管理器或实施重启策略。 有关这些的更多信息,请参阅 Docker 文档中的 自动启动容器。
当使用运行相同 Docker 映像的多个主机进行大规模工作时,使用 Ansible 或 Chef 等配置管理工具自动执行步骤会更有效。 要了解有关配置管理的更多信息,请参阅 An Introduction to Configuration Management 和 Automating Server Setup with Ansible: A DigitalOcean Workshop Kit。
除了在每台主机上构建相同的镜像之外,您还可以使用像 Docker Hub 这样的镜像注册表来简化部署,它可以集中构建、存储和分发 Docker 镜像到多个服务器。 与映像注册表一起,持续集成和部署管道可以帮助您构建、测试映像并将其部署到应用服务器。 有关 CI/CD 的更多信息,请参阅 CI/CD 最佳实践简介。