使用 Docker + Nginx + Certbot 实现自动化管理 SSL 证书

devtools/2025/2/4 16:41:46/

使用 Docker + Nginx + Certbot 实现自动化管理 SSL 证书

在互联网安全环境日益重要的今天,为站点或应用部署 HTTPS 已经成为一种常态。然而,手动申请并续期证书既繁琐又容易出错。本文将以 Nginx + Certbot 为示例,基于 Docker 容器来搭建一个可以自动申请、自动续期的 SSL 证书服务,帮助你更轻松地管理多域名的 HTTPS。


一、方案简介

在这个方案中,我们使用官方 Nginx 镜像作为基础镜像,并在其中安装 Python3、Certbot 及阿里云 DNS 插件。通过以下几个关键脚本来实现自动部署及管理:

  1. start.sh:容器启动时执行,初始化阿里云 DNS 配置并启动 Nginx。
  2. add-domain.sh:添加新域名并为其申请证书,自动生成 Nginx 配置文件,最后重载 Nginx。
  3. deploy-certbot.sh:读取域名列表,一键申请或续期证书。
  4. domains.txt:存放所有需要申请 SSL 证书的域名列表。

同时,通过挂载相应目录,可以将证书、日志、Nginx 配置文件全部持久化到宿主机,不会因为容器的更新或重启而丢失。


二、准备工作

  1. 服务器已经安装 Docker(版本建议 >= 19.03)。

  2. 你拥有阿里云账号,并且已经开通了 AliyunDNSFullAccess 权限的 Access Key:

    • ALIYUN_ACCESS_KEY_ID
    • ALIYUN_ACCESS_KEY_SECRET
  3. 确保服务器安全组或本地防火墙已允许 HTTP(80) 和 HTTPS(443)。


三、获取配置文件

以下操作假设我们把部署目录统一放在 /data/nginx-certbot 下。

# 1. 创建部署目录
mkdir -p /data/nginx-certbot
cd /data/nginx-certbot# 2. 通过临时容器获取镜像内的 Nginx 配置
docker run -d --name nginx_conf -it registry.cn-hangzhou.aliyuncs.com/hbteck/nginx-certbot:1.20.2# 3. 拷贝 Nginx 配置文件到本地 conf 目录
docker cp nginx_conf:/etc/nginx/ ./conf# 4. 删除临时容器
docker rm -f nginx_conf

此时,/data/nginx-certbot/conf 目录下已包含镜像内的默认 Nginx 配置文件(包括 nginx.conf、vhost 目录等),可根据需求进行调整和定制。


四、主要文件和脚本说明

下面列举一些在本方案中常见或关键的文件结构与作用,仅供参考:

  1. Dockerfile
    这是我们自定义镜像的核心文件。可自行修改或参考原始镜像中的配置。示例:

    dockerfile"># 使用官方nginx镜像作为基础镜像
    FROM nginx:stable# 替换为国内源
    RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list && \sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list# 安装必要的软件包
    RUN apt-get update && \apt-get install -y \python3 \python3-pip \cron \&& rm -rf /var/lib/apt/lists/*# 配置pip国内源
    RUN pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \pip3 config set install.trusted-host mirrors.aliyun.com# 安装certbot和阿里云DNS插件
    RUN pip3 install certbot certbot-dns-aliyun# 创建必要的目录
    RUN mkdir -p /etc/nginx/conf.d && \mkdir -p /etc/letsencrypt && \mkdir -p /etc/nginx/vhost && \mkdir -p /var/log/certbot# 复制配置文件
    COPY ./nginx.conf /etc/nginx/nginx.conf
    COPY ./deploy-certbot.sh /usr/local/bin/deploy-certbot.sh
    COPY ./add-domain.sh /usr/local/bin/add-domain.sh
    COPY ./start.sh /usr/local/bin/start.sh
    COPY ./domains.txt /etc/letsencrypt/domains.txt# 设置脚本权限
    RUN chmod +x /usr/local/bin/deploy-certbot.sh && \chmod +x /usr/local/bin/start.sh && \chmod +x /usr/local/bin/add-domain.sh# 设置环境变量默认值
    ENV RENEW_CRON_SCHEDULE="0 0 1 * * ?"
    ENV CERTBOT_EMAIL="admin@example.com"
    ENV DNS_PROPAGATION_SECONDS=60VOLUME ["/etc/letsencrypt", "/var/log/certbot"]CMD ["/usr/local/bin/start.sh"]
  2. add-domain.sh
    在容器内用于添加新域名并申请证书的核心脚本:

    #!/bin/bashnew_domain=$1
    backend_url=${2:-"http://localhost:8080"}  # 默认后端地址
    local_ip=$3  # 第三参数: 本地IP地址# 检查参数
    if [ -z "$new_domain" ]; thenecho "用法: add-domain.sh <domain> [backend_url] [local_ip]"echo "示例: add-domain.sh example.com http://localhost:8080 192.168.1.1"exit 1
    fi# 验证域名格式
    if ! echo "$new_domain" | grep -P '(?=^.{4,253}$)(^(?:[a-zA-Z0-9](?:(?:[a-zA-Z0-9\-]){0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$)' > /dev/null; thenecho "错误: 无效的域名格式"exit 1
    fi# 检查域名是否已存在
    if grep -q "^${new_domain}$" /etc/letsencrypt/domains.txt; thenecho "域名 ${new_domain} 已存在"exit 0
    fi# 添加新域名
    echo "$new_domain" >> /etc/letsencrypt/domains.txt
    echo "已添加域名: $new_domain"# 为新域名获取证书
    if ! /usr/local/bin/deploy-certbot.sh; thenecho "证书获取失败,移除域名配置"sed -i "\|^${new_domain}$|d" /etc/letsencrypt/domains.txtexit 1
    fi# 创建nginx配置
    if [ -z "$local_ip" ]; thencat <<EOT > /etc/nginx/vhost/${new_domain}.conf
    # upstream配置
    upstream ${new_domain}_backend {server ${backend_url#http://};keepalive 32;
    }server {listen 443 ssl http2;server_name ${new_domain};ssl_certificate /etc/letsencrypt/live/${new_domain}/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/${new_domain}/privkey.pem;ssl_trusted_certificate /etc/letsencrypt/live/${new_domain}/chain.pem;# SSL配置ssl_session_timeout 1d;ssl_session_cache shared:SSL:50m;ssl_session_tickets off;ssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;ssl_prefer_server_ciphers off;# HSTS配置add_header Strict-Transport-Security "max-age=63072000" always;location / {proxy_pass ${backend_url};proxy_http_version 1.1;proxy_set_header Connection "";proxy_set_header Host \$host;proxy_set_header X-Real-IP \$remote_addr;proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto \$scheme;}
    }server {listen 80;server_name ${new_domain};return 301 https://\$server_name\$request_uri;
    }
    EOT
    elsecat <<EOT > /etc/nginx/vhost/${new_domain}.conf
    server {listen 443 ssl http2;server_name ${new_domain};ssl_certificate /etc/letsencrypt/live/${new_domain}/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/${new_domain}/privkey.pem;ssl_trusted_certificate /etc/letsencrypt/live/${new_domain}/chain.pem;# SSL配置ssl_session_timeout 1d;ssl_session_cache shared:SSL:50m;ssl_session_tickets off;ssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;ssl_prefer_server_ciphers off;# HSTS配置add_header Strict-Transport-Security "max-age=63072000" always;location / {proxy_pass ${backend_url};proxy_http_version 1.1;proxy_set_header Connection "";proxy_set_header Host \$host;proxy_set_header X-Real-IP \$remote_addr;proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto \$scheme;}error_page 500 502 503 504 /50x.html;location = /50x.html {root /usr/share/nginx/html;}location /api/ {proxy_pass http://${local_ip}:18500/;proxy_set_header Host \$http_host;proxy_set_header X-Real-IP \$remote_addr;proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto \$scheme;proxy_set_header X-NginX-Proxy true;# 传递所有原始请求头信息proxy_pass_request_headers on;}location /nacos/ {proxy_pass http://${local_ip}:8848/nacos/;proxy_set_header Host \$http_host;proxy_set_header X-Real-IP \$remote_addr;proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto \$scheme;proxy_set_header X-NginX-Proxy true;}location /kafka/ {proxy_pass http://${local_ip}:9000/kafka/;proxy_set_header Host \$http_host;proxy_set_header X-Real-IP \$remote_addr;proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto \$scheme;proxy_set_header X-NginX-Proxy true;}
    }server {listen 80;server_name ${new_domain};return 301 https://\$server_name\$request_uri;
    }
    EOT
    fi# 检查nginx配置
    if ! nginx -t; thenecho "Nginx配置检查失败,移除配置文件"rm -f /etc/nginx/vhost/${new_domain}.confsed -i "\|^${new_domain}$|d" /etc/letsencrypt/domains.txtexit 1
    fi# 重新加载nginx配置
    nginx -s reloadecho "域名 ${new_domain} 配置完成"
  3. deploy-certbot.sh
    该脚本读取 /etc/letsencrypt/domains.txt 里的域名,通过 DNS-01 方式批量申请或更新证书。

    #!/bin/bash# 检查aliyun配置文件
    if [ ! -f "/etc/letsencrypt/aliyun.ini" ]; thenecho "错误: 未找到阿里云DNS凭证文件"exit 1
    fi# 检查domains.txt是否存在且不为空
    if [ ! -s "/etc/letsencrypt/domains.txt" ]; thenecho "警告: domains.txt为空或不存在"exit 0
    fi# 设置默认值
    : ${DNS_PROPAGATION_SECONDS:="60"}
    : ${CERTBOT_EMAIL:="admin@example.com"}# 处理每个域名
    while IFS= read -r domain || [ -n "$domain" ]; do# 跳过空行和注释[[ -z "$domain" || "${domain:0:1}" == "#" ]] && continueecho "正在处理域名: $domain"certbot certonly \--authenticator dns-aliyun \--dns-aliyun-credentials /etc/letsencrypt/aliyun.ini \--dns-aliyun-propagation-seconds "$DNS_PROPAGATION_SECONDS" \--non-interactive \--agree-tos \--email "$CERTBOT_EMAIL" \-d "$domain" \--expand \--keep-until-expiring \--preferred-challenges dns-01 \--config-dir /etc/letsencrypt \--logs-dir /var/log/certbot \--work-dir /var/lib/certbotif [ $? -eq 0 ]; thenecho "成功为 $domain 获取/更新证书"elseecho "为 $domain 获取/更新证书失败"# 如果获取证书失败,则退出脚本exit 1fi
    done < "/etc/letsencrypt/domains.txt"# 检查nginx配置并重新加载
    nginx -t && nginx -s reload
  4. start.sh
    容器启动时自动执行的脚本:

    #!/bin/bash# 检查必要的环境变量
    if [ -z "$ALIYUN_ACCESS_KEY_ID" ] || [ -z "$ALIYUN_ACCESS_KEY_SECRET" ]; thenecho "错误: 未设置阿里云访问密钥环境变量"exit 1
    fi# 创建 Aliyun 配置文件
    mkdir -p /etc/letsencrypt
    cat <<EOT > /etc/letsencrypt/aliyun.ini
    dns_aliyun_access_key = $ALIYUN_ACCESS_KEY_ID
    dns_aliyun_access_key_secret = $ALIYUN_ACCESS_KEY_SECRET
    EOT# 设置严格的权限
    chmod 600 /etc/letsencrypt/aliyun.ini# 设置默认值
    : ${RENEW_CRON_SCHEDULE:="0 0 1 * *"}
    : ${CERTBOT_EMAIL:="admin@example.com"}
    : ${DNS_PROPAGATION_SECONDS:="60"}# 确保domains.txt存在
    touch /etc/letsencrypt/domains.txt# 初始获取或更新 SSL 证书
    /usr/local/bin/deploy-certbot.sh# 配置证书自动续期
    echo "$RENEW_CRON_SCHEDULE /usr/local/bin/deploy-certbot.sh >> /var/log/certbot/renew.log 2>&1" > /etc/cron.d/certbot-renew
    chmod 0644 /etc/cron.d/certbot-renew# 创建日志目录
    mkdir -p /var/log/certbot
    touch /var/log/certbot/renew.log# 启动 cron 服务
    service cron start# 启动 nginx
    exec nginx -g "daemon off;"
    
  5. /etc/nginx/

    • nginx.conf:Nginx 的主配置文件

      	
      #=============================================start 全局块==================================================================#运行用户
      #user  nobody;
      #worker进程数量,通常设置为cpu核数相等
      worker_processes  4;#全局错误文件
      #error_log  logs/error.log;
      #error_log  logs/error.log  notice;
      #error_log  logs/error.log  info;
      #pid位置
      #pid        logs/nginx.pid;
      #=============================================end 全局块=====================================================================#=============================================start events块================================================================
      events {# 单个worker进程最大并发连接数worker_connections  1024;
      }
      #=============================================end events块==================================================================#=============================================start http块==================================================================http {#引入mime类型的定义文件include       mime.types;default_type  application/octet-stream;#设置日志格式#log_format  main  ' -  [] "" '#                  '  "" '#                  '"" ""';#access_log  logs/access.log  main;sendfile        on;#tcp_nopush     on;#连接超时时间#keepalive_timeout  0;keepalive_timeout  65;#开启gzip压缩#gzip  on;#上传大小限制client_max_body_size 512m;#自定义变量 $connection_upgrademap $http_upgrade $connection_upgrade {default          keep-alive;  #默认为keep-alive 可以支持 一般http请求'websocket'      upgrade;     #如果为websocket 则为 upgrade 可升级的。#default upgrade;#''      close;}#引入配置文件include  vhost/*.conf;
      }#=============================================end http块=================================================================
      
    • vhost 目录:根据域名生成的各子配置文件

  6. domains.txt

    • 存放所有需要申请证书的域名,每行一个。

五、运行容器

准备就绪后,可以执行以下命令来启动容器:

docker run -d \-p 80:80 \-p 443:443 \--name nginx-certbot \-e ALIYUN_ACCESS_KEY_ID=<你的阿里云AccessKeyId> \-e ALIYUN_ACCESS_KEY_SECRET=<你的阿里云AccessKeySecret> \-e CERTBOT_EMAIL=<你的邮箱> \-e RENEW_CRON_SCHEDULE="0 0 1 * *" \-v /data/nginx-certbot/certs:/etc/letsencrypt \-v /data/nginx-certbot/log-certbot:/var/log/certbot \-v /data/nginx-certbot/log:/var/log/nginx \-v /data/nginx-certbot/conf:/etc/nginx \-v /data/nginx-certbot/html:/usr/share/nginx/html \registry.cn-hangzhou.aliyuncs.com/hbteck/nginx-certbot:1.20.2

参数说明:

  1. -p 80:80 和 -p 443:443:将宿主机的 80/443 端口映射给容器,用于 HTTP/HTTPS。
  2. -e ALIYUN_ACCESS_KEY_ID / ALIYUN_ACCESS_KEY_SECRET:阿里云 DNS 插件需要的密钥。
  3. -e CERTBOT_EMAIL:用于跟证书签发相关的提示信息接收。
  4. -e RENEW_CRON_SCHEDULE:设置证书自动续期的 cron 表达式(默认为每月1日00:00 点执行 certbot renew)。
  5. -v /data/nginx-certbot/conf:/etc/nginx:持久化 Nginx 配置文件,方便宿主机直接修改。
  6. -v /data/nginx-certbot/certs:/etc/letsencrypt:持久化证书文件,让重启或更新不会丢失证书。
  7. 其他挂载可根据自身需求调整。

成功运行后,容器会自动加载已有 Nginx 配置并尝试运行部署脚本(如果 domains.txt 里已有域名)。


六、添加域名及证书

当容器正常运行后,通过以下命令添加新域名并生成对应的 HTTPS 配置:

docker exec nginx-certbot /usr/local/bin/add-domain.sh <domain> [backend_url] [local_ip]

示例:

docker exec nginx-certbot /usr/local/bin/add-domain.sh hbteck.com \http://172.19.160.13:17111/ 172.19.160.13
  • :必填,目标域名。
  • [backend_url]:可选,域名后端服务的转发地址,默认 http://localhost:8080。
  • [local_ip]:可选,用于特定场景自动生成更多配置规则。

该脚本会完成以下操作:

  1. 将新域名写入 /etc/letsencrypt/domains.txt。
  2. 调用 /usr/local/bin/deploy-certbot.sh 进行 DNS-01 方式的证书申请。
  3. 成功后在 /etc/nginx/vhost/ 下写入对应的 vhost 配置文件,再重载 Nginx。

如果一切正常,可以在浏览器访问 https://hbteck.com 测试效果。


七、查看与管理证书

  1. 查看证书详情:

    docker exec nginx-certbot certbot certificates
    

    如果成功,会列出每个域名证书所在路径及过期时间。

  2. 手动重载 Nginx 配置:

    docker exec -it nginx-certbot /usr/sbin/nginx -s reload
    
  3. 手动执行续期:

    docker exec -it nginx-certbot certbot renew --deploy-hook 'nginx -s reload'
    

    一般不需要手动运行,脚本中已经配置了 cron 任务每天检查并每月1日00:00自动续期。


八、常见问题及解决方案

  1. 申请证书失败:

    • 请确认域名的 DNS 解析在阿里云上,并且 AccessKey 拥有 AliyunDNSFullAccess 权限。
    • 检查是不是填写了错误的 Access Key 信息。
    • 域名是否在 /etc/letsencrypt/domains.txt 中已存在,如果有问题可删除后重试。
  2. 80 和 443 端口冲突:

    • 如果宿主机已占用 80/443 端口,可修改映射端口,如 -p 8080:80 -p 8443:443,但需要相应修改 nginx.conf。
  3. 自定义 Nginx 配置:

    • 直接修改宿主机 /data/nginx-certbot/conf/nginx.conf 或 vhost/文件,完成后执行 docker exec nginx-certbot nginx -s reload。
  4. 脚本执行权限:

    • 如果提示权限不足,可在宿主机手动 chmod +x /data/nginx-certbot/conf/*.sh 等路径,或在 Dockerfile 中确保脚本均可执行。

九、总结

通过在 Docker 中整合 Nginx + Certbot + 阿里云 DNS 插件,我们可以轻松实现多域名自动化申请及续期 SSL 证书的流程。该方案具有以下优点:

  • DNS-01 验证方式无需对外暴露除 80/443 以外的端口,也不影响原有服务。
  • 对多域名、多环境非常灵活,可以快速添加新域名。
  • 证书续期完全自动化,无需人工干预。

只要按照上述步骤,下载配置,将 Access Key 写入环境变量,启动容器并执行 add-domain.sh 脚本,即可轻松接入新的 HTTPS 域名。祝你在实际生产环境中使用顺利,助力网站与应用更安全、稳定地运行!


http://www.ppmy.cn/devtools/156055.html

相关文章

2025_2_3 C语言中关于枚举类型,动态内存分配

1.枚举类型 枚举类型是用来表示一些离散值的 枚举常量默认从0开始&#xff0c;依次递增1 typedef enum{s1,s2,s3,s4 }num;以上&#xff1a;s10,s2 1, s3 2, s4 3 可以为枚举常量显式指定值 typedef enum{s1 1,s2 2,s3 3,s4 4 }num;枚举常量的调用有两种方法 直接使用…

15 刚体变换模块(rigid.rs)

rigid.rs是一个表示三维刚体变换&#xff08;Rigid Transformation&#xff09;的结构体定义&#xff0c;用于在计算机图形学、机器人学以及物理模拟等领域中表示物体在三维空间中的旋转和平移。在这个定义中&#xff0c;所有长度在变换后都保持不变&#xff0c;这是刚体变换的…

Qt文件操作

目录 一、文件操作相关类 1.QFile 2.QFileInfo 3.QTextStream 4.QDataStream 5.QDir 6.QFileSystemWatcher 7.QTemporaryFile 二、文件操作示例 1.文本文件操作 2.目录操作 3.二进制文件操作 一、文件操作相关类 1.QFile QFile类用于文件的创建、读写、复制、删除…

Python面试宝典13 | Python 变量作用域,从入门到精通

今天&#xff0c;我们来深入探讨一下 Python 中一个非常重要的概念——变量作用域。理解变量作用域对于编写清晰、可维护、无 bug 的代码至关重要。 什么是变量作用域&#xff1f; 简单来说&#xff0c;变量作用域就是指一个变量在程序中可以被访问的范围。Python 中有四种作…

基于微信小程序的酒店管理系统设计与实现(源码+数据库+文档)

酒店管理小程序目录 目录 基于微信小程序的酒店管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员模块的实现 (1) 用户信息管理 (2) 酒店管理员管理 (3) 房间信息管理 2、小程序序会员模块的实现 &#xff08;1&#xff09;系统首页 &#xff…

JVM-运行时数据区

JVM的组成 运行时数据区-总览 Java虚拟机在运行Java程序过程中管理的内存区域&#xff0c;称之为运行时数据区。 《Java虚拟机规范》中规定了每一部分的作用 运行时数据区-应用场景 Java的内存分成哪几部分&#xff1f; Java内存中哪些部分会内存溢出&#xff1f; JDK7 和J…

如何解决云台重力补偿?

如何解决云台重力补偿? 最近在调试步兵云台的时候,由于枪管、图传、摄像头等重力的原因,pitch轴的参数尤其难以调整,又不想抬升和降低使用两套不同的参数,所以使用了重力补偿,效果也是比较理想的,于是整理为一篇文章记录一下 一、问题根源:枪管重力在“搞事情” 想象…

SpringBoot 配置文件

目录 一. 配置文件相关概念 二. 配置文件快速上手 1. 配置文件的格式 2. properties 配置文件 (1) properties 基本语法 (2) 读取配置文件内容 (3) properties 缺点分析 3. yml配置文件 (1) yml 基本语法 (2) 读取配置文件内容 (3) yml 配置对象 (4) yml 配置集合 …