使用 Docker、NGINX、Route 53 和 Let's Encrypt 部署服务

导航至

本文由 Jeremy White 撰写,最初于 2021 年 4 月 13 日发表在 Network to Code 博客上。

Docker 是部署应用程序或服务的强大工具,并且有许多 Docker 编排工具可用于帮助简化已部署容器的管理。但是,如果您只想部署少量服务,而不希望为了运行少量容器而承担设置和管理另一个应用程序堆栈的负担,该怎么办?我将介绍如何在单个 Docker 主机上部署少量服务。我部署的服务包括:Let's Encrypt 用于生成通配符证书,Route 53 用于注册 ACNAME 记录,以及 NGINX 用于执行带有 SNI 封装的反向代理。我之前在 Raspberry Pi 上的容器中部署了其中一些服务,作为我的 水族箱控制器 的一部分,但我希望为部署提供更好的灵活性,并且不将自己局限于仅部署 ARM 兼容的容器。再加上希望在家中部署持久服务,这促使我构建了一个运行 Ubuntu 20.04 LTS 和 Docker 的新物理 Linux 主机。

这篇文章旨在展示 Docker 的灵活性,而不是使用 Docker 部署的生产指南。我强烈建议在部署容器时使用编排,并且如果我正在处理多个主机或管理更多服务,我也会通过容器编排进行部署。

在这篇文章中,我将使用我的 InfluxDB 服务作为示例服务。此服务通过 NGINX 反向代理从 Docker 主机外部使用,也用于东西向容器到容器的通信。此外,我的所有服务都通过 docker-compose 定义为代码,以便提供比原始 docker run 命令更轻松的体验。

通信

下面是一个通信流程的示例,用户使用部署在 Docker 中的服务,该服务然后使用同一主机上的另一个后端服务。最终用户不知道流量是如何流动的,而作为管理员,您可以利用容器到容器的通信。

An example of a communication flow for a user consuming a service deployed in Docker that then consumes another backend service on the same host

Route 53

我正在使用 Route 53 注册我的所有 DNS 记录;这通过不运行像 Bind 9 DNS 这样的服务,简化了我在本地管理的服务的数量。我在 Route53 中发布的所有记录都解析为私有 IP 地址,并且无法从我的网络外部路由。在我的示例中,我为物理主机本身设置了一个 A 记录,所有服务都是指向服务器 A 记录的 CNAME 记录。我的域名是在 Route 53 注册的,这有助于简化流程。

Docker 容器名称解析

Docker 提供了 Docker 守护程序内部的网络,以及为同一 Docker 网络上的容器执行容器名称解析的能力。为了简化这些支持服务的声明,我正在使用 docker-compose;并且为了在容器内进行东西向通信,我只需将流量发送到相邻的服务名称。在上面的图表中,Grafana 服务通过 http://influxdb:8086/ 与 InfluxDB 服务通信。Docker 将主机名 influxdb 解析为 InfluxDB 容器的 IP 地址。

Let's Encrypt

尽管服务部署在我的本地家庭网络上,并且位于适当的防火墙之后,但我更喜欢从第一天起就使用 TLS 部署服务。这是我在企业环境中工作的第一天就灌输给我的最佳实践。在我的示例中,我正在使用 CertBot 来请求和管理我的证书。为了简化证书管理,我正在为所有服务使用 *.whitej6.com 的通配符证书。此外,TLS 的一个扩展是 服务器名称指示SNI

NGINX 和 SNI

所有旨在从 Docker 守护程序外部使用的服务都仅在服务的端口定义中暴露给 localhost。这确保了所有我想允许进入 Docker 的流量都必须来自物理主机或部署到主机的应用程序。我需要暴露的每个服务都在 NGINX 配置文件中都有自己的定义。该配置告诉 NGINX 使用哪个证书,哪个请求的服务器名称映射到哪个底层 localhost 端口号。当 NGINX 接收到 HTTPS 请求时,它首先通过 SNI 确定请求的服务,然后执行反向代理到 localhost 上的正确端口。这允许我在物理主机上终止 TLS,并从 NGINX 到底层 Docker 服务运行纯文本协议。通过在容器外部以安全方式执行我自己的 TLS 终止,它可以简化容器部署,减少自定义供应商提供的容器的需求,和/或弄清楚每个供应商如何在他们的容器中执行 TLS 终止。

示例部署

使用的示例是我在家中托管的服务的实际描述。

先决条件

本示例将不涵盖如何安装 Ubuntu、Docker、docker-compose、CertBot 或 NGINX。这些项目都有完善的文档安装说明,应参考这些文档。本示例将涵盖这些服务的使用。

Route 53

我正在使用 Route 53 来托管 Docker 堆栈中部署的应用程序所需的 DNS 记录。下面是一个表,列出了部署所需的两个记录。在示例的后续步骤中,我将创建生成证书所需的第三个记录。

记录类型 目标
A ubuntu-server.whitej6.com 10.0.0.16
CNAME influxdb.whitej6.com ubuntu-server.whitej6.com

您可以在输出中看到 influxdb.whitej6.comubuntu-server.whitej6.com 的 CNAME,它解析为 10.0.0.16

?  ~ dig influxdb.whitej6.com

; <<>> DiG 9.10.6 <<>> influxdb.whitej6.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61162
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;influxdb.whitej6.com.		IN	A

;; ANSWER SECTION:
influxdb.whitej6.com.	236	IN	CNAME	ubuntu-server.whitej6.com.
ubuntu-server.whitej6.com. 238	IN	A	10.0.0.16

;; Query time: 21 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Tue Apr 06 13:44:41 CDT 2021
;; MSG SIZE  rcvd: 93

?  ~

生成证书

使用 Cerbot 生成我的 Let's Encrypt 证书提供了一个简单的端到端解决方案,用于生成和管理签名证书。有几种不同的质询可用于验证域所有权。由于我对 Certbot 的经验很少,并且我可以轻松地管理我的域中的 DNS 记录,所以我选择使用 DNS 质询。在下面的示例中,您将看到我发出命令,Certbot 响应一个 TXT 记录,我需要为 _acme-challenge.whitej6.com 创建,值为 XXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX。创建记录后,我可以按 Enter 继续,Certbot 将执行质询并验证 TXT 记录的值是否与预期值匹配。Let's Encrypt 证书是有效期为 90 天的短期证书。Certbot 支持通过 certbot renew 命令续订证书。

?  ~ sudo certbot -d "*.whitej6.com" --manual --preferred-challenges dns certonly
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Requesting a certificate for *.whitej6.com
Performing the following challenges:
dns-01 challenge for whitej6.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.whitej6.com with the following value:

XXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/whitej6.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/whitej6.com/privkey.pem
   Your certificate will expire on 2021-07-05. 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"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.openssl.ac.cn/donate
   Donating to EFF:                    https://eff.org/donate-le

?  ~

部署容器

容器的部署和管理是通过 docker-compose 完成的,以简化 Docker 的管理。

~/influxdb/docker-compose.yml

---
version: "3"
services:
  influxdb:
    image: influxdb:latest
    restart: always
    ports:
      - "127.0.0.1:8086:8086"
    volumes:
      - "/influxdb:/var/lib/influxdb"

我想确保每个已部署的服务都可以根据需要与其他服务通信。为了实现这一点,我在部署中使用相同的 compose 项目名称。如果每个服务都部署在另一个 compose 项目中,则名称解析和东西向通信将变得复杂。在下面的输出中,您将看到 Docker 对在 docker-compose.yml 文件之外声明服务不太满意,这是可以预期的。

?  ~ export COMPOSE_PROJECT_NAME="server"
?  ~ docker-compose -f influxdb/docker-compose.yml up -d
Building with native build. Learn about native build in Compose here: https://docs.docker.net.cn/go/compose-native-build/
WARNING: Found orphan containers (server_grafana_1, server_gitlab_1, server_redis_1, server_registry_1, server_netbox_1, server_postgres_1) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up.
Starting server_influxdb_1  ... done
?  ~

配置 NGINX

我个人喜欢保持我的基本 NGINX 配置文件为默认值,并使用 conf.d 文件夹中的单个 .conf 文件来覆盖我需要的内容。我在其自己的文件中声明每个覆盖,以便我更容易识别和进行更改。下面的文件路径基于 NGINX 的默认安装位置。您的安装可能使用不同的路径,但概念是相同的。

/etc/nginx/conf.d/redirect.conf

强制所有 HTTP 流量重定向到 HTTPS,并保持请求的主机不变。

server {
    listen 80 default_server;
    return 301 https://$host$request_uri;
}

/etc/nginx/conf.d/influxdb.conf

map $http_x_forwarded_proto $thescheme {
    default $scheme;
    https https;
}

server {
    # Inbound requested hostname
    server_name influxdb.whitej6.com;

    listen 443 ssl;

    # Let's Encrypt certificate location
    ssl_certificate /etc/letsencrypt/live/whitej6.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/whitej6.com/privkey.pem;

    client_max_body_size 25m;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $thescheme;
    add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';

    # Where to reverse proxy HTTP traffic to
    location / {
        proxy_pass http://localhost:8086;
    }

}

重启 NGINX

一旦 NGINX 的配置完成,您需要重启 NGINX 服务。我正在使用 systemctl 来管理主机上运行的系统服务。

?  ~ sudo systemctl restart nginx
?  ~

验证堆栈

?  influxdb echo "curl from outside the docker network"
curl from outside the docker network
?  influxdb curl https://influxdb.whitej6.com/health
{"checks":[],"message":"ready for queries and writes","name":"influxdb","status":"pass","version":"1.8.4"}%
?  influxdb
?  influxdb echo "curl from inside the docker network"
curl from inside the docker network
?  influxdb docker exec -it server_gitlab_1 sh -c "curl http://influxdb:8086/health"
{"checks":[],"message":"ready for queries and writes","name":"influxdb","status":"pass","version":"1.8.4"%
?  influxdb

在部署和验证所有服务后,我拥有一个功能齐全的多租户 Docker 主机,通过 HTTPS 安全地为每个服务提供服务,并使用 DNS 记录来提高可用性。这使我能够将 Grafana 和 InfluxDB 服务从托管我的水族箱控制器的 Raspberry Pi 迁移到 Docker 主机,以改善用户体验。希望这篇文章能帮助您安全地将服务部署到 Docker 主机。