使用 Docker、NGINX、Route 53 和 Let's Encrypt 部署服务
作者:社区 / 产品,用例,开发者
2021年4月26日
导航至
本文由 Jeremy White 撰写,并最初于 2021 年 4 月 13 日发表在 Network to Code 博客。
Docker 是部署应用程序或服务的强大工具,目前有许多 Docker 调度工具可以帮助简化已部署容器的管理。但如果您只想部署少量服务,并且不希望设置和管理另一个应用程序堆栈仅为了运行几个容器,那该怎么办呢?我将介绍我是如何在单个 Docker 主机上部署了一组服务。我部署的服务包括 Let’s Encrypt 生成通配符证书,Route 53 注册 A 和 CNAME 记录,以及 NGINX 使用 SNI 封装进行反向代理。我之前在树莓派上以我的 Aquarium Controller 部分部署了一些这些服务在容器中,但我想提供更好的部署灵活性,而不仅仅是局限于部署 ARM 兼容的容器。结合在家部署持久服务的需求,这促使我构建了一个新的物理 Linux 主机,运行 Ubuntu 20.04 LTS & Docker。
本文旨在展示 Docker 的灵活性,而不是部署 Docker 的生产指南。我强烈建议在部署容器时使用调度,如果我在处理多个主机或管理更多服务的情况下工作,我将通过容器调度来部署。
在本篇博文中,我将使用我的 InfluxDB 服务作为示例服务。该服务通过 NGINX 反向代理从 Docker 主机外部使用,也用于东西向容器间的通信。此外,我的所有服务都通过 docker-compose
编码定义,以提供比原始 docker run
命令更轻松的体验。
通信
以下是一个示例,说明用户消费 Docker 中部署的服务,然后消费同一主机上的另一个后端服务。最终用户并不知道流量是如何流动的,而作为管理员,您可以利用容器间的通信。
Route 53
我正在使用Route 53来注册所有DNS记录;这简化了我本地管理服务的数量,因为我没有运行像Bind 9 DNS这样的服务。我在Route 53中发布的所有记录都解析为私有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使用哪个证书,哪个请求的服务器名称映射到哪个本地主机端口。当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.com
是ubuntu-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响应我需要为_acme-challenge.whitej6.com
创建一个TXT记录,其值为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 https://127.0.0.1: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服务从托管我的水族控制器的主板上移到Docker主机上,以改善用户体验。希望这篇文章能帮助您安全地将服务部署到Docker主机上。