使

使用 Let's Encrypt 和 Nginx 自动进行 SSL

2025-05-24

使用 Let's Encrypt 和 Nginx 自动进行 SSL

请参阅帖子底部的更新摘要以了解变更日志。

注:该项目的 v2 版本已于 2020 年 12 月发布letsencrypt-nginx-proxy-companion。我已更新本文以反映此更新,但旧的 v1 版本代码仍保留在页脚。如果您需要升级现有机器,请参阅nginx-proxy/docker-letsencrypt-nginx-proxy-companion仓库。


自 2016 年以来,证书颁发机构 Let's Encrypt 一直提供免费的 SSL/TLS 证书,旨在让网络上的加密通信无处不在。如果你曾经购买过证书,你就会知道它们通常价格不菲,验证过程也极其繁琐,而且它们还会在你度假时过期,导致网络中断。

有了 Let's Encrypt,所有这些问题都迎刃而解。这要归功于自动化证书管理环境 (ACME) 协议,它能够自动执行证书的验证和部署,从而节省您的时间和金钱。ACME 本身就是一个有趣的话题,您可以在这里了解更多关于各种验证方法(称为挑战)的信息,但今天我将向您展示如何轻松设置一个能够自动生成、验证和部署证书的反向代理。

我们要做的第一件事是创建一个名为的用户定义桥接网络service-network。我不擅长命名,但这似乎很适用。

docker network create --subnet 10.10.0.0/24 service-network
Enter fullscreen mode Exit fullscreen mode

图A

接下来,我们将设置一个反向代理。反向代理只是接受请求,并根据路由规则(例如哪些主机名应该分配给哪些服务容器)将其代理到另一个服务上。我猜它看起来有点像这样:

反向代理(如我想象的那样)

我们将使用 Jason Wilder 的镜像jwilder/nginx-proxy。这是一个维护良好的项目,拥有丰富的文档。我们将端口 80 和 443 映射到容器中,以便同时处理 HTTP 和 HTTPS 连接。我们也有一些卷,其中三个是标准 Docker 卷,一个是 Docker 守护进程的 UNIX 套接字。

你可能注意到我经常在脚本中使用长格式的参数。虽然我们可以轻松地将其放在一行中以节省空间,但这对将来可能需要维护脚本的其他人来说并没有什么帮助。

docker run \
  --detach \
  --restart always \
  --publish 80:80 \
  --publish 443:443 \
  --name nginx-proxy \
  --network service-network \
  --volume /var/run/docker.sock:/tmp/docker.sock:ro \
  --volume nginx-certs:/etc/nginx/certs \
  --volume nginx-vhost:/etc/nginx/vhost.d \
  --volume nginx-html:/usr/share/nginx/html \
  jwilder/nginx-proxy
Enter fullscreen mode Exit fullscreen mode

图B

好的,现在我们有了反向代理,接下来需要设置 Let's Encrypt 配套程序,我们将使用 Yves Blusseau 的镜像jrcs/letsencrypt-nginx-proxy-companion。我已经完美地使用了它近一年。

我们将相同的卷映射到此容器,但未发布任何端口。我们将NGINX_PROXY_CONTAINER环境变量设置为与代理容器的名称匹配,仅此而已。每当检测到新的服务容器时,此容器都会运行一个进程,生成证书并使其保持最新。

docker run \
  --detach \
  --restart always \
  --name nginx-proxy-letsencrypt \
  --network service-network \
  --volumes-from nginx-proxy \
  --volume /var/run/docker.sock:/var/run/docker.sock:ro \
  --volume /etc/acme.sh \
  --env "DEFAULT_EMAIL=mail@yourdomain.tld" \
  jrcs/letsencrypt-nginx-proxy-companion
Enter fullscreen mode Exit fullscreen mode

图C

最后,是时候添加一个服务容器了。我们将使用tutum/hello-world一个非常流行的镜像进行测试。为此,我们需要一个配置了 DNS 的主机名。我们将假装使用hello-world.example.com并假装我们已经设置了一条A指向该服务器 IP 地址的记录。

docker run \
  --detach \
  --restart always \
  --name hello-world \
  --network service-network \
  --env VIRTUAL_HOST=hello-world.example.com \
  --env LETSENCRYPT_HOST=hello-world.example.com \
  --env LETSENCRYPT_EMAIL="youremail@example.com" \
  tutum/hello-world
Enter fullscreen mode Exit fullscreen mode

图D

那么,让我们来看看这里发生了什么。我们设置了一个代理容器,它监听端口 80 和 443。然后,我们设置了一个 ACME 证书容器,它将为我们管理证书。接着,我们创建了一个 hello world 容器。我们将环境变量设置VIRTUAL_HOST为我们的主机名,代理会获取该主机名,以便将请求路由到这个容器。请求通过我们用户定义的网桥进行路由service-network。我们还设置了环境变量LETSENCRYPT_HOST和 ,LETSENCRYPT_EMAILACME 容器会获取这些变量,并在获取证书时使用它们。请注意,这两个主机名必须始终匹配(VIRTUAL_HOSTLETSENCRYPT_HOST)。

我们要做的就是将这三个变量添加到容器中,它将被代理和 ACME 容器检测到,并且很快就会起作用。

现在需要注意几点。端口发现——代理如何知道要使用哪个端口?我们使用的 hello-world 镜像在 Dockerfile 中使用 暴露了一个端口EXPOSE 80。这始终优先,因此如果镜像有暴露的端口,就会使用它。但是,如果您的 Dockerfile 未定义暴露的端口,或者您可能在运行时使用环境变量配置了端口(例如HTTP_PORT=8000),那么您可以使用--expose参数让代理知道要使用哪个端口。如果有多个可用端口,您可以使用环境变量 指定要使用的端口VIRTUAL_PORT

再次重申,DockerfileEXPOSE优先于--expose参数。

现在,当你点击 时https://hello-world.example.com,DNS 服务器会返回一条包含你公网 IP 的 A 记录,浏览器会连接到这个 IP 地址的 443 端口,然后主机会将该连接路由到nginx-proxy容器。这会终止 SSL 连接,并将请求hello-world作为普通的 HTTP 请求代理到容器。

图E

最后,我们还可以做一件事。代理服务器会处理从 HTTP 到 HTTPS 的升级,这很棒,但有时您需要处理 www/apex 域名的重定向。您可以在一些域名注册商的控制面板中配置此功能,但最终您的主机名会得到不同的 IP 地址。

有一个更好的解决方案,这次我们要使用的图像实际上是我的一个,adamkdean/redirect

它的工作方式非常简单。你只需设置另一个容器,将其设置为要重定向VIRTUAL_HOST的域名,并配置其目标地址即可。例如,我们希望它始终重定向到某个域名,因为我们喜欢 www。example.comwww.example.com

我们有像这样的主图像,绑定到www.example.com

docker run \
  --detach \
  --restart always \
  --name example-website \
  --network service-network \
  --env VIRTUAL_HOST=www.example.com \
  example/website
Enter fullscreen mode Exit fullscreen mode

然后,我们创建重定向伴随容器,绑定到example.com,并将环境变量REDIRECT_LOCATION设置为我们的首选目的地并REDIRECT_STATUS_CODE进行适用设置。

docker run \
  --detach \
  --restart always \
  --name example-redirect \
  --network service-network \
  --env VIRTUAL_HOST=example.com \
  --env REDIRECT_LOCATION="http://www.example.com" \
  --env REDIRECT_STATUS_CODE=301 \
  adamkdean/redirect
Enter fullscreen mode Exit fullscreen mode

现在发生的情况是 的请求命中example.com此重定向容器,该REDIRECT_STATUS_CODE容器以 和进行响应REDIRECT_LOCATION

我们当然可以使用 Let's Encrypt 来实现这些。

docker run \
  --detach \
  --restart always \
  --name example-website \
  --network service-network \
  --env VIRTUAL_HOST=www.example.com \
  --env LETSENCRYPT_HOST=www.example.com \
  --env LETSENCRYPT_EMAIL="youremail@example.com" \
  example/website

docker run \
  --detach \
  --restart always \
  --name example-redirect \
  --network service-network \
  --env VIRTUAL_HOST=example.com \
  --env LETSENCRYPT_HOST=example.com \
  --env LETSENCRYPT_EMAIL="youremail@example.com" \
  --env REDIRECT_LOCATION="https://www.example.com" \
  --env REDIRECT_STATUS_CODE=301 \
  adamkdean/redirect
Enter fullscreen mode Exit fullscreen mode

在这种情况下,初始的 请求会example.com命中代理,代理会终止 SSL 连接,并example-redirect通过代理转发到容器,容器会返回 301 错误码www.example.com。下一个 请求,也就是 ,也会www.example.com命中代理,并通过代理转发到example-website容器,如下所示:

图F

希望以上内容对您有所帮助。此设置适用于单个站点或多个站点。我通常在搭建大型容器平台有点过头的情况下使用这种方法(对于博客来说,Kubernetes 应该没问题,对吧?)。

它的更新和部署超级简单,用了一年多,效果非常好。感谢阅读。


更新(2020 年 2 月 21 日):根据要求,这是一个docker-compose.yml版本。

version: "3"

services:
  web:
    image: example/website
    expose:
     - 8000
    environment:
      HTTP_PORT: 8000
      VIRTUAL_HOST: www.example.com
      LETSENCRYPT_HOST: www.example.com
      LETSENCRYPT_EMAIL: "example@example.com"
    networks:
      service_network:

  web-redirect:
    image: adamkdean/redirect
    environment:
      VIRTUAL_HOST: example.com
      LETSENCRYPT_HOST: example.com
      LETSENCRYPT_EMAIL: "example@example.com"
      REDIRECT_LOCATION: "https://www.example.com"
    networks:
      service_network:

  nginx-proxy:
    image: jwilder/nginx-proxy
    ports:
      - 80:80
      - 443:443
    container_name: nginx-proxy
    networks:
      service_network:
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - nginx-certs:/etc/nginx/certs
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html

  nginx-proxy-letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    environment:
      NGINX_PROXY_CONTAINER: "nginx-proxy"
    networks:
      service_network:
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - nginx-certs:/etc/nginx/certs
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html

networks:
  service_network:

volumes:
  nginx-certs:
  nginx-vhost:
  nginx-html:
Enter fullscreen mode Exit fullscreen mode

更新(2020年2月28日):

我意识到我在谈论 时犯了一个错误VIRTUAL_PORT。我已经更新了文档,但需要澄清的是,你需要使用EXPOSEDockerfile 中的命令和--exposeDocker 命令行界面上的参数来告诉代理要使用哪个端口。如果暴露了多个端口,则可以使用VIRTUAL_PORT环境变量来指示要使用哪个暴露的端口。

抱歉造成混淆!


更新(2020年4月22日):

在安全重定向代码片段中添加了一些缺失的反斜杠。哎呀!


更新(2020年12月11日):

letsencrypt-nginx-proxy-companion 的 v1 代码。

docker run \
  --detach \
  --restart always \
  --name nginx-proxy-letsencrypt \
  --network service-network \
  --volume /var/run/docker.sock:/var/run/docker.sock:ro \
  --volume nginx-certs:/etc/nginx/certs \
  --volume nginx-vhost:/etc/nginx/vhost.d \
  --volume nginx-html:/usr/share/nginx/html \
  --env NGINX_PROXY_CONTAINER="nginx-proxy" \
  jrcs/letsencrypt-nginx-proxy-companion:v1.13.1
Enter fullscreen mode Exit fullscreen mode

如果您喜欢这篇文章,您可能会喜欢我的下一篇文章《调试 Docker 容器》,其中我们将深入探讨几种方法,让您可以弄清楚我们称之为容器的黑匣子里面到底发生了什么。

关于注入 docker.sock 的注意事项:docker 守护进程的 UNIX 套接字 (/var/run/docker.sock) 授予 docker 访问权限,换句话说,就是 root 权限。无论何时运行第三方镜像,请务必确认您授予的权限。

文章来源:https://dev.to/adamkdean/automatic-ssl-with-let-s-encrypt-nginx-4nfk
PREV
构建你自己的虚拟滚动 - 第一部分 第一部分 什么是窗口?让我们来做一些简单的数学运算 示例代码性能和动态高度
NEXT
Python 包管理器比较 📦 简介 优点 Poetry Hatch PDM Rye GitHub 统计 结论和要点