首先创建一个用于存放 Nginx 相关配置、证书等文件的文件夹,并在文件夹下创建如下的 docker-compose.yml:

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    restart: unless-stopped
    volumes:
      - ./nginx/conf/:/etc/nginx/conf.d/:ro
      - ./certbot/www:/var/www/certbot/:ro
      - ./certbot/conf/:/etc/nginx/ssl/:ro
  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./certbot/www/:/var/www/certbot/:rw
      - ./certbot/conf/:/etc/letsencrypt/:rw

这里大概解释一下这个文件描述的配置:

将宿主机的 80、443 端口绑定到 Nginx 容器的 80、443 端口,用于处理 HTTP 与 HTTPS 请求。
将当前目录下的 ./nginx/conf 文件夹挂载到 Nginx 容器内的配置文件夹,用于存储 Nginx 配置文件。所有这些挂载的文件夹如果不存在,docker compose 都会自动创建。
将当前目录下的 ./certbot/www 同时挂载到 Nginx 容器及 Certbot 容器。这个目录是用于 SSL 验证的。CA 颁发证书给你的前提,是你能证明这个域名确实属于你。因此,一般会通过下发给你特定文件,并要求你将文件放入服务器特定位置,再由 CA 读取文件来完成身份验证。这里使用的 Certbot 容器就是为了简化这一验证步骤,自动化地完成上述验证流程。因此,需要一个共享文件夹,来完成验证文件到 Nginx 服务器的传递。
将当前目录下的 ./certbot/conf/ 同时挂载到 Nginx 容器及 Certbot 容器。这个目录将会用于存储 Certbot 申请的证书。
如果你按照我接下来的步骤遇到了什么问题,也可以访问开头提到的 Minders Blog 一步一步构建 docker-compose.yml,但是按照我的理解,直接采用最终版就可以了。

Step 1:初次启动
在根目录(也就是docker-compose.yml 所在的位置)通过 docker compose up -d 启动容器。第一次启动时,会首先从网络下拉 Nginx 与 Certbot 容器的镜像,耐心等待启动完成即可。启动完成后,上述三个文件夹应该被自动创建好了。

Step 2:修改 Nginx 配置文件
在 ./nginx/conf 这个路径下创建一个配置文件,例如 nginx.conf,并填入以下内容:

server {
    listen 80;
    listen [::]:80;

    server_name example.org;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://example.org$request_uri;
    }
}

别忘记把所有的 example.org 替换为你自己的域名。

这个文件定义了一个 server,监听在 IPv4 和 IPv6 的 80 端口,对应的域名是 example.org。
后面两条 location 比较重要。
第一条定义了当访问 http://example.org/.well-known/acme-challenge/ 时,应该展示 /var/www/certbot 中的内容,这是用于 CA 验证。
第二条定义了其余所有位置的访问,都应该被 301 重定向到 https://example.org,并保留末尾的 $request_uri。
这样,用户通过 HTTP 协议访问你的域名时,就会被重定向到 HTTPS 站点。当然,目前我们还没有配置 Nginx 的 HTTPS 服务器,所以重定向后只能访问到默认界面。

如果你要申请的证书不是指向你的 Apex Domain(不带任何次级域名),那你可以先暂停在这里继续读下去,后面会讲次级域名 SSL 证书的申请。

Step 3:申请 SSL 证书
通过 docker compose restart 重启两个容器。此时,Nginx 的配置应该已经生效了。接着运行

docker compose run --rm  certbot certonly --webroot --webroot-path /var/www/certbot/ --dry-run -d example.org

模拟颁发证书的流程。别忘了将 example.org 换成你自己的域名,并且确保DNS解析正常,通过域名能正常访问到你的 Nginx 服务器。Certbot 可能会询问你的信息,用于在 Let’s Encrypt 注册,正常填写即可。如果最后看到 successful,说明模拟过程没有问题,就可以去掉 --dry-run 参数,真正完成证书颁发流程了。

docker compose run --rm  certbot certonly --webroot --webroot-path /var/www/certbot/ -d example.org

配置 Nginx 反向代理
在上一步中申请到域名证书后,我们就可以继续修改 Nginx 的配置文件,配置反向代理了:

server {
    listen 80;
    listen [::]:80;

    server_name example.org www.example.org;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://example.org$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name example.org;

    ssl_certificate /etc/nginx/ssl/live/example.org/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/example.org/privkey.pem;
    
    location / {
        proxy_pass http://<local IP>:<local port>/;
    }
}

讲一下新增的部分。

新增的 server,监听在 IPv4 和 IPv6 的 443 端口,并且开启了 SSL 与 HTTP2,对应的域名是 example.org。

后面两条 ssl_certificate 指向你上一步申请到的 SSL 证书。注意,路径中的 example.org 也要替换为你的真实域名。如果不确定的话,可以 docker exec -it bash 进入 Nginx 容器确认一下路径。

后面一条 location 实现了反响代理的部分。将 替换为你本地运行的 HTTP 服务,当用户通过 https://example.org 访问你的网站时,实际访问到的是反向代理的页面,并且通过 SSL 加密。

注意:因为 docker 创建了虚拟子网,因此宿主机上的服务并不能直接通过 127.0.0.1 访问。

可以通过 ip a 指令检查宿主机的所有网络界面,并找到宿主机在 docker 子网中的 IP,例如 172.17.0.1/16。
如果不需要隔离容器与宿主机网络,可以去掉 docker-compose.yml 中 ports 的部分,并将网络模式设置为 host,这里不多赘述,别忘记通过 docker compose up -d 重新创建容器。然后就可以通过 127.0.0.1 直接访问宿主机服务。
如果要暴露 80(HTTP)、443(HTTPS)以外的端口,同样要修改 docker-compose.yml 中 ports 的部分,并通过docker compose up -d 重新创建容器。
通过 docker compose restart 重启 Nginx 容器后,即可实现通过 SSL 反向代理本地服务。

提示

如果使用 Cloudflare CDN 为博客加速,需要在 SSL/TLS 选项中将加密模式选中为 Full (strict),在 Flexible 模式下,Cloudflare 会试图访问你的 HTTP 站点,导致循环重定向,最终用户浏览器会报「重定向过多」错误。

为子域名申请证书及配置反向代理
通常我们会将站点的子域名指向不同的服务,并通过 Nginx 统一反向代理,例如 overleaf.example.org 指向本地部署的 Overleaf、qb.example.org 指向 qBitTorrent 的 WebUI 等。想为子域名申请 SSL 证书,和上面的流程基本一致。首先修改 nginx.conf,继续加入对应子域名的服务器:

# ......
server {
    listen 80;
    listen [::]:80;

    server_name <subdomain>.example.org;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        proxy_pass http://<local IP>:<local port>/;
    }
}

如果你在这里配置的子域名是 www,并且希望子域名和 Apex Domain 都指向你的主服务(例如博客),可以在 301 重定向的部分直接指向你的 https://example.org

重复上述的申请证书流程,就能申请到对应子域名 .example.org 的证书了。然后继续添加 HTTPS 服务器:

# ......
server {
    listen 80;
    listen [::]:80;

    server_name <subdomain>.example.org;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://<subdomain>.example.org$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name <subdomain>.example.org;

    ssl_certificate /etc/nginx/ssl/live/<subdomain>.example.org/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/<subdomain>.example.org/privkey.pem;

    location / {
        # return 301 https://example.org$request_uri;
        return 301 https://<subdomain>.example.org$request_uri;
    }

}

如果是 www 子域名,可以 301 重定向到你的主域名。如果是其他服务,可以反向代理到本地的其他服务。别忘记 docker compose restart 应用配置修改。

证书续签
Let’s Encrypt 颁发的证书有效期比较短,别忘记及时续签。通过 docker compose run --rm certbot renew 命令,即可通过 Certbot 自动续签全部证书。同样可以加上 --dry-run 参数模拟续签流程。