Docker Compose 部署certbot实现TLS证书签发和续期

132 阅读5分钟

前提条件

  • 有现成的域名、公网IP
  • 可访问公网的服务器
  • 可用的邮箱

开始部署

1. Certbot 镜像拉取

docker pull certbot/certbot:v5.2.2

2. 创建 docker-compose.yml 文件

services:
  nginx:
    image: nginx:1.25.4
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro                  # 和服务器时间同步
      - /root/docker/nginx/nginx/:/etc/nginx/             # 主配置目录
      - /root/docker/nginx/nginx/certs/:/etc/letsencrypt  # 证书配置目录
      - /root/docker/nginx/html/:/usr/share/nginx/html    # 网站根目录
      - /root/docker/nginx/log/:/var/log/nginx            # 日志目录
    networks:
      - nginx-network
    restart: always
    entrypoint: ["/docker-entrypoint.sh"]
    command: ["nginx", "-g", "daemon off;"]
    stop_signal: SIGQUIT
    logging:
      driver: json-file
    networks: 
      - nginx-network
    depends_on:
      - certbot   # 确保 Certbot 先启动(非必须)

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - /root/docker/nginx/nginx/certs/:/etc/letsencrypt      # 证书存储目录(与 Nginx 共享)
      - /root/docker/nginx/html/:/var/www/html                # HTTP 验证所需的 Webroot
    networks: 
      - nginx-network
    restart: always

networks:
  nginx-network:
    external: true
    driver: bridge

如果使用的是openresty,则使用下面的配置

services:
  openresty:
    container_name: openresty
    deploy:
        resources:
            limits:
                cpus: 5
                memory: '256M'
    image: registry.cn-qingdao.aliyuncs.com/ling/openresty:1.27.1.2-focal #替换为自身镜像
    environment:
      TZ: Asia/Shanghai
    network_mode: host
    restart: always
    volumes:
        - ./nginx/conf.d:/etc/nginx/conf.d/:rw
        - ./nginx/conf:/usr/local/openresty/nginx/conf/:rw
        - ./nginx/html:/usr/local/openresty/nginx/html:rw
        - ./nginx/logs:/usr/local/openresty/nginx/logs/:rw
        - ./nginx/lua:/usr/local/openresty/nginx/modules/:rw
        - ./nginx/certs:/usr/local/openresty/nginx/certs/:rw

  certbot:
    image: certbot/certbot:v5.2.2
    container_name: certbot
    volumes:
      - ./nginx/certs/:/etc/letsencrypt      # 证书存储目录(与 Nginx 共享)
      - ./nginx/html/:/var/www/html                # HTTP 验证所需的 Webroot
      - ./nginx/logs/certbot/:/var/log/letsencrypt
    network_mode: host
    restart: on-failure:3
    entrypoint: ["sh", "-c"]
    # 避免容器启动后退出(休眠)
    command: ["tail", "-f", "/dev/null"]

3. 修改 nginx.conf 或 创建 tansuo.conf 文件

server {
    listen 80;
    server_name tansuo.com;  # 替换为你的域名
    #配置http验证可访问
    location ^~ /.well-known/acme-challenge/ {
        #此目录都是nginx容器内的目录,对应宿主机volumes中的http验证目录,而宿主机的又与certbot容器中命令--webroot-path指定目录一致,从而就整个串起来了,解决了http验证问题
        root /etc/letsencrypt; #openresty对应为/usr/local/openresty/nginx/html
    }
    # 其他请求重定向到HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

4. 启动服务

sudo docker compose -f docker-compose.yml up -d

5. 测试证书发放

运行以下命令测试证书发放是否正常:

sudo docker compose run --rm certbot certonly --webroot --webroot-path /etc/letsencrypt --dry-run -d tansuo.com
或者
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/html --dry-run -d tansuo.com

如果输出 The dry run was successful.,说明测试成功。

如果出现::404字样,首先检查yml配置是否和文章中的匹配,其次检查域名和公网IP是否正确绑定。最后重启nginx服务。

6. 正式获取证书

如果测试成功,去掉 --dry-run 参数正式获取证书:

docker compose run --rm certbot certonly --webroot --webroot-path /etc/letsencrypt -d tansuo.com

按照提示输入邮箱等信息,完成证书申请。

7. 生成证书文件

Certbot 会在 ./nginx/certs/live/tansuo.com/ 目录下生成证书文件,包括:

  • fullchain.pem:完整的证书链
  • privkey.pem:私钥

8. 创建 HTTPS 配置文件

在你自定义的配置文件,比如上文创建的tansuo.conf 中配置 HTTPS信息:

server {
    listen       443 ssl;
    server_name  tansuo.com;

    ssl_certificate /etc/letsencrypt/live/xxx.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/xxx.com/privkey.pem;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /api/ {
        proxy_pass http://gateway:18080/;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

将此文件挂载到 Nginx 的配置目录中,我的tansuo.conf位于conf.d目录下。

9. 重启 Nginx

重新加载 Nginx 配置以应用 HTTPS:

docker compose restart nginx

10. 配置定时任务自动续期

为了避免证书过期,创建一个检查脚本,设置定时任务为每月 1 号凌晨 2 点检查证书是否过期&自动续期证书&重启 Nginx: vi renew-certbot.sh

#!/bin/bash

# Certbot SSL 证书自动续期脚本(针对90天有效期优化)
# 证书有效期:90天,推荐每30天检查续期

echo "=== SSL 证书自动续期脚本开始执行 ==="
echo "执行时间: $(date)"
echo "证书类型: Let's Encrypt (90天有效期)"

# 设置变量
LOG_FILE="/var/log/certbot-renew.log"
COMPOSE_DIR="/root/docker"

# 记录开始时间
START_TIME=$(date +%s)

# 检查必要目录
if [ ! -d "$COMPOSE_DIR" ]; then
    echo "错误: Docker Compose 目录不存在: $COMPOSE_DIR" | tee -a "$LOG_FILE"
    exit 1
fi

# 切换到工作目录
cd "$COMPOSE_DIR" || {
    echo "错误: 无法切换到目录 $COMPOSE_DIR" | tee -a "$LOG_FILE"
    exit 1
}

echo "工作目录: $(pwd)" | tee -a "$LOG_FILE"

# 检查 Docker 服务
if ! systemctl is-active --quiet docker; then
    echo "错误: Docker 服务未运行,尝试启动..." | tee -a "$LOG_FILE"
    systemctl start docker
    sleep 10
fi

# 检查证书过期时间(可选)
echo "检查证书状态..." | tee -a "$LOG_FILE"
docker compose run --rm certbot certificates 2>&1 | grep -E "(EXPIRED|VALID|Domains)" | tee -a "$LOG_FILE"

# 执行证书续期(--quiet 模式减少输出)
echo "开始证书续期检查..." | tee -a "$LOG_FILE"
docker compose run --rm certbot renew --quiet --non-interactive 2>&1 | tee -a "$LOG_FILE"

RENEW_RESULT=${PIPESTATUS[0]}

if [ $RENEW_RESULT -eq 0 ]; then
    echo "✓ 证书续期检查完成" | tee -a "$LOG_FILE"
    
    # 检查是否有证书被更新
    if docker compose run --rm certbot certificates 2>&1 | grep -q "待续期"; then
        echo "检测到证书需要更新,重启 Nginx..." | tee -a "$LOG_FILE"
        
        # 重启 Nginx
        docker compose restart nginx 2>&1 | tee -a "$LOG_FILE"
        
        if [ ${PIPESTATUS[0]} -eq 0 ]; then
            echo "✓ Nginx 重启成功" | tee -a "$LOG_FILE"
            # 发送通知(可选)
            echo "证书续期成功 - $(date)" >> /tmp/cert-renew-success.log
        else
            echo "⚠ Nginx 重启失败,请手动检查" | tee -a "$LOG_FILE"
        fi
    else
        echo "ℹ 无需续期,证书仍在有效期内" | tee -a "$LOG_FILE"
    fi
else
    echo "✗ 证书续期失败,退出码: $RENEW_RESULT" | tee -a "$LOG_FILE"
    # 可以在这里添加报警通知
    exit $RENEW_RESULT
fi

# 计算执行时间
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

echo "脚本执行完成,耗时: ${DURATION}秒" | tee -a "$LOG_FILE"
echo "完成时间: $(date)" | tee -a "$LOG_FILE"
echo "========================================" | tee -a "$LOG_FILE"

设置定时任务:

crontab -e

添加以下内容:

# Let's Encrypt 证书自动续期(90天有效期)
# 每月1号凌晨2点检查(足够安全)
0 2 1 * * /root/renew-certbot.sh >/dev/null 2>&1

# 日志清理(每月清理一次旧日志)
0 3 1 * * find /var/log -name "certbot-renew.*.log" -mtime +30 -delete

保存后,定时任务将自动运行。


总结

通过以上步骤,使用 Certbot 为你的网站配置了TLS证书,并设置了自动续期任务。确保网站始终安全可靠,避免因证书过期导致的服务中断。

无法使用80、443端口情况

如果公司网络无法开放80、443端口,可以使用DNS-01方式进行验证,具体步骤如下:

一、前提条件

  • Certbot 5.2.2 + 阿里云 DNS 插件 0.38.1
  • 域名管理平台账号密码(需要开通AccessKey)
  • CDN(域名+端口配置)

1. 阿里云 RAM 子用户配置

  • 已创建 RAM 子用户,授予 AliyunDNSFullAccess 权限(仅 DNS 管理,最小权限);
  • 保存子用户的 AccessKey IDAccessKey Secret

二、步骤 1:编写 Dockerfile(构建带阿里云插件的 Certbot 镜像)

基于 Certbot 5.2.2 官方镜像(Alpine 系统),安装兼容的 certbot-dns-aliyun==0.38.1

在 docker-compose.yml同级目录下创建 Dockerfile

# 基础镜像:Certbot 5.2.2 官方镜像(Alpine系统,最小体积)
FROM certbot/certbot:v5.2.2

# 安装阿里云DNS插件依赖 + 插件本身(适配5.2.2)
RUN apk update && apk add --no-cache \
    python3-dev \
    py3-pip \
    gcc \
    musl-dev \
    libffi-dev \
    openssl-dev && \
    # 安装指定版本插件(官网验证兼容Certbot 5.2.2)
    pip3 install --no-cache-dir certbot-dns-aliyun==0.38.1 && \
    # 删除编译依赖,减小镜像体积
    apk del gcc musl-dev libffi-dev openssl-dev && \
    # 清理缓存
    rm -rf /var/cache/apk/*

# 容器启动后休眠(避免空命令报错,不影响证书操作)
CMD ["tail", "-f", "/dev/null"]

自行构建镜像,我是推送镜像到个人阿里云镜像仓库,然后给docker-compose.yml使用。

三、步骤 2:修改 docker-compose.yml

version: '3.8'

services:
  certbot:
    image: registry.cn-qingdao.aliyuncs.com/ling/certbot-aliyun-dns:1.0
    volumes:
      - ./nginx/certs/:/etc/letsencrypt      # 证书存储目录(核心)
      - ./nginx/html/:/var/www/html          # Webroot(备用)
      - ./nginx/logs/:/var/log/letsencrypt   # 日志目录
    # 网络模式:host(确保容器能访问阿里云DNS API)
    network_mode: host
    # 重启策略:失败仅重启3次
    restart: on-failure:3
    # 覆盖ENTRYPOINT为sh,所有命令由shell解析 
    entrypoint: ["sh", "-c"]
    # 避免容器启动后退出(休眠)
    command: ["tail", "-f", "/dev/null"]

四、步骤 3:配置文件+构建镜像 + 启动容器(验证基础环境)

# 1. 配置文件
在./nginx/certs目录下新建certbot-dns.ini文件
sudo vi ./nginx/certs/certbot-dns.ini
# 2. 输入上文新建RAM用户获取的key和secret:
dns_aliyun_access_key = LTAI5tMUHuXMBR
dns_aliyun_access_key_secret = juK8Q6ASdBX1iRBofLNF14T5
# 3. 设置Certbot强制要求的600权限(仅root可读可写) 
sudo chmod 600 ./nginx/certs/aliyun-dns.ini 
sudo chown root:root ./nginx/certs/aliyun-dns.ini
# 4. 启动Certbot容器
sudo docker compose up -d
# 5. 验证容器状态(输出 Up 即正常)
sudo docker compose ps
# 正常输出:certbot   Up (healthy)   ...

五、步骤 4:核心验证(确保插件 + 环境变量正常)

验证 1:插件是否安装成功

docker compose run --rm certbot "certbot plugins | grep -i aliyun"

✅ 正确输出(说明插件识别正常):

* dns-aliyun
Description: Obtain certificates using a DNS TXT record (if you are using Aliyun DNS).
Interfaces: Authenticator, Plugin
Entry point: EntryPoint(name='dns-aliyun', value='certbot_dns_aliyun.dns_aliyun:Authenticator', group='certbot.plugins')

六、步骤 5:DNS-01 验证(Dry-Run 测试,无风险)

先执行模拟验证(不生成真实证书),确保阿里云 DNS 权限、网络、插件均正常:

sudo docker compose run --rm certbot "certbot certonly \
  --authenticator dns-aliyun \
  --dns-aliyun-credentials /etc/letsencrypt/aliyun-dns.ini \
  --dns-aliyun-propagation-seconds 60 \
  --agree-tos \
  --no-eff-email \
  --email gentlelions@163.com \
  -d official.acc.cn \
  --dry-run
"

✅ 正确输出(说明 DNS 验证流程正常):

Account registered.
Requesting a certificate for official.acc.cn
Performing the following challenges:
dns-01 challenge for domain official.acc.cn
Waiting 60 seconds for DNS changes to propagate
Successfully authorized domain official.acc.cn
Dry run: skipping deploy hook.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled HTTPS on https://official.acc.cn
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

七、步骤 6:生成正式证书

删除 --dry-run 参数,执行正式证书生成:

sudo docker compose run --rm certbot "certbot certonly \
  --authenticator dns-aliyun \
  --dns-aliyun-credentials /etc/letsencrypt/aliyun-dns.ini \
  --dns-aliyun-propagation-seconds 60 \
  --agree-tos \
  --no-eff-email \
  --email gentlelions@163.com \
  -d official.acc.cn \
"

✅ 验证证书是否生成:

ls -l ./nginx/certs/live/official.acc.cn/

正常输出(包含证书文件):

total 4
-rw-r--r-- 1 root root  692 Jan 30 15:00 README
lrwxrwxrwx 1 root root   44 Jan 30 15:00 cert.pem -> ../../archive/official.acc.cn/cert1.pem
lrwxrwxrwx 1 root root   45 Jan 30 15:00 chain.pem -> ../../archive/official.acc.cn/chain1.pem
lrwxrwxrwx 1 root root   49 Jan 30 15:00 fullchain.pem -> ../../archive/official.acc.cn/fullchain1.pem
lrwxrwxrwx 1 root root   47 Jan 30 15:00 privkey.pem -> ../../archive/official.acc.cn/privkey1.pem

八、步骤 7:配置自动续期

Let's Encrypt 证书有效期 90 天,需配置定时任务自动续期,续期后重启 Nginx(若使用 Nginx)。

1. 创建续期脚本

./nginx 目录下创建 certbot-renew.sh

#!/bin/bash
set -e  # 遇到错误立即退出,避免无效执行

# ===================== 配置项(根据你的环境修改)=====================
# Certbot工作目录(与docker-compose.yml挂载路径一致)
CERTBOT_DIR="/docker/nginx"
# OpenResty容器名(替换为你实际的容器名,如openresty、nginx等)
OPENRESTY_CONTAINER="openresty"
# 日志文件路径(自动创建,记录续期过程)
RENEW_LOG="${CERTBOT_DIR}/renew.log"
# ===================================================================

# 确保日志目录存在
mkdir -p "${CERTBOT_DIR}"

# 记录脚本启动时间
echo -e "\n========================================" >> "${RENEW_LOG}"
echo "Certbot 续期脚本执行时间:$(date +'%Y-%m-%d %H:%M:%S')" >> "${RENEW_LOG}"

# 切换到Certbot工作目录
cd "${CERTBOT_DIR}" || {
    echo "错误:无法进入Certbot工作目录 ${CERTBOT_DIR}" >> "${RENEW_LOG}"
    exit 1
}

# 执行Certbot续期(仅续期<30天的证书),捕获退出码
# 退出码说明:
# 0 = 无证书需续期(所有证书有效)
# 1 = 有证书续期成功
# 2 = 续期失败(网络/权限/配置错误)
docker compose run --rm certbot "certbot renew \
  --authenticator dns-aliyun \
  --dns-aliyun-credentials /etc/letsencrypt/aliyun-dns.ini \
  --dns-aliyun-propagation-seconds 60" >> "${RENEW_LOG}" 2>&1
RENEW_EXIT_CODE=$?

# 根据退出码执行不同逻辑
case ${RENEW_EXIT_CODE} in
    0)
        echo "结果:无证书需要续期(所有证书有效期>30天),无需重启OpenResty" >> "${RENEW_LOG}"
        ;;
    1)
        echo "结果:证书续期成功,开始重启OpenResty..." >> "${RENEW_LOG}"
        # 重启OpenResty容器(仅续期成功时执行)
        docker restart "${OPENRESTY_CONTAINER}" >> "${RENEW_LOG}" 2>&1
        if [ $? -eq 0 ]; then
            echo "成功:OpenResty 重启完成" >> "${RENEW_LOG}"
        else
            echo "错误:OpenResty 重启失败,请手动检查容器状态" >> "${RENEW_LOG}"
        fi
        ;;
    2)
        echo "结果:证书续期失败!请查看日志 ${RENEW_LOG} 排查问题" >> "${RENEW_LOG}"
        ;;
    *)
        echo "结果:未知错误,退出码 ${RENEW_EXIT_CODE},请手动检查" >> "${RENEW_LOG}"
        ;;
esac

echo "脚本执行结束:$(date +'%Y-%m-%d %H:%M:%S')" >> "${RENEW_LOG}"

### 2. 赋予脚本执行权限

```bash
sudo chmod +x ./nginx/certbot-renew.sh

3. 添加系统定时任务(crontab)

# 编辑crontab
sudo crontab -e

添加以下内容(每月 1 号、15 号凌晨 3 点执行续期,避免证书过期):

# Certbot自动续期(每月1/15号凌晨3点执行)
0 3 1,15 * * /bin/bash /docker/nginx/certbot-renew.sh >> /docker/nginx/renew.log 2>&1

4. 验证定时任务

sudo crontab -l  # 输出上述添加的内容即正常

九、故障排查

1. 续期失败?查看日志

# Certbot日志
cat ./nginx/logs/letsencrypt/letsencrypt.log
# 续期脚本日志
cat ./renew.log

2. 阿里云 DNS 权限问题?

  • 确认 RAM 子用户已授予 AliyunDNSFullAccess(不是只读权限);
  • 确认域名 official.acc.cn 归属该阿里云账号。

3. 容器网络不通?

  • 检查 network_mode: host 是否配置,或替换为 network: bridge + 端口映射;
  • 验证容器内能否访问阿里云 API:docker compose run --rm certbot ping dns.aliyuncs.com