强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Certbot 证书自动化教程 / 第 11 章:Docker 部署

第 11 章:Docker 部署

11.1 Docker 环境概述

在容器化部署中,Certbot 通常以独立容器运行,与 Web 服务器容器(Nginx/Apache)通过共享卷交换证书文件。

Docker 部署架构

┌─────────────────────────────────────────────────┐
│                 Docker Host                      │
│                                                  │
│  ┌──────────────────┐   ┌─────────────────────┐ │
│  │  Nginx 容器       │   │  Certbot 容器       │ │
│  │                   │   │                     │ │
│  │  /etc/letsencrypt │◄─│  certbot renew      │ │
│  │  /var/www/certbot │──│                     │ │
│  │                   │   │  定期执行续期        │ │
│  └──────┬────────────┘   └─────────────────────┘ │
│         │                                        │
│    共享卷:                                        │
│    - letsencrypt-data: /etc/letsencrypt          │
│    - certbot-webroot: /var/www/certbot           │
│         │                                        │
└─────────┼────────────────────────────────────────┘
          │
          ▼
     外部请求:80/:443

Docker 方式的优势

优势说明
隔离性Certbot 不污染宿主机环境
一致性镜像版本确定,避免依赖问题
可移植可在任何支持 Docker 的平台运行
易更新拉取新镜像即可升级
多实例不同站点可使用不同的 Certbot 配置

11.2 Certbot Docker 基础

基本命令

# 拉取官方镜像
docker pull certbot/certbot

# 查看版本
docker run --rm certbot/certbot --version

# 查看帮助
docker run --rm certbot/certbot --help

申请证书(Standalone 模式)

docker run --rm -it \
  -p 80:80 \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/log/letsencrypt:/var/log/letsencrypt \
  certbot/certbot certonly --standalone \
  -d example.com \
  --agree-tos \
  --email admin@example.com

申请证书(Webroot 模式)

docker run --rm -it \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/log/letsencrypt:/var/log/letsencrypt \
  -v /var/www/certbot:/var/www/certbot \
  certbot/certbot certonly --webroot \
  -w /var/www/certbot \
  -d example.com \
  --agree-tos \
  --email admin@example.com

DNS 验证申请通配符证书

docker run --rm -it \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/log/letsencrypt:/var/log/letsencrypt \
  -v /etc/letsencrypt/cloudflare:/etc/letsencrypt/cloudflare:ro \
  certbot/certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
  -d example.com \
  -d "*.example.com" \
  --agree-tos \
  --email admin@example.com

查看证书

docker run --rm \
  -v /etc/letsencrypt:/etc/letsencrypt \
  certbot/certbot certificates

续期

docker run --rm \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/log/letsencrypt:/var/log/letsencrypt \
  -v /var/www/certbot:/var/www/certbot \
  certbot/certbot renew

Docker 卷挂载说明

容器路径宿主机路径用途
/etc/letsencrypt/etc/letsencrypt 或 Docker volume证书和配置存储
/var/log/letsencrypt/var/log/letsencrypt 或 Docker volume日志
/var/www/certbot/var/www/certbot 或 Docker volumeWebroot 验证文件

11.3 Docker Compose 完整部署

项目目录结构

/opt/certbot-deploy/
├── docker-compose.yml
├── nginx/
│   ├── nginx.conf
│   ├── conf.d/
│   │   └── default.conf
│   └── ssl/
│       └── options-ssl-nginx.conf
├── certbot/
│   └── cli.ini
└── www/
    └── certbot/

Docker Compose 配置

# /opt/certbot-deploy/docker-compose.yml
version: "3.8"

services:
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-webroot:/var/www/certbot:ro
      - certbot-certs:/etc/letsencrypt:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - certbot
    restart: unless-stopped
    networks:
      - web

  certbot:
    image: certbot/certbot:latest
    container_name: certbot
    volumes:
      - certbot-certs:/etc/letsencrypt
      - certbot-webroot:/var/www/certbot
      - certbot-logs:/var/log/letsencrypt
      - ./certbot/cli.ini:/etc/letsencrypt/cli.ini:ro
    # 默认命令(初始申请证书时使用,之后会覆盖为续期命令)
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done'"
    restart: unless-stopped
    networks:
      - web

volumes:
  certbot-certs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /opt/certbot-deploy/data/certs
  certbot-webroot:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /opt/certbot-deploy/data/webroot
  certbot-logs:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /opt/certbot-deploy/data/logs

networks:
  web:
    driver: bridge

创建数据目录

sudo mkdir -p /opt/certbot-deploy/data/{certs,webroot,logs}
sudo mkdir -p /opt/certbot-deploy/nginx/conf.d
sudo mkdir -p /opt/certbot-deploy/nginx/ssl
sudo mkdir -p /opt/certbot-deploy/certbot
sudo mkdir -p /opt/certbot-deploy/www/certbot

Nginx 配置

# /opt/certbot-deploy/nginx/nginx.conf
user  nginx;
worker_processes  auto;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
}
# /opt/certbot-deploy/nginx/conf.d/default.conf
server {
    listen 80;
    server_name example.com www.example.com;

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

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/nginx/ssl/options-ssl-nginx.conf;

    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;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    root /usr/share/nginx/html;
    index index.html;
}

Certbot 全局配置

# /opt/certbot-deploy/certbot/cli.ini
email = admin@example.com
agree-tos = true
non-interactive = true

初始证书申请

# Step 1: 先创建一个临时的 Nginx 配置(仅 HTTP)
cat > /opt/certbot-deploy/nginx/conf.d/default.conf << 'EOF'
server {
    listen 80;
    server_name example.com www.example.com;

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

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

# Step 2: 启动 Nginx
cd /opt/certbot-deploy
docker compose up -d nginx

# Step 3: 申请证书
docker compose run --rm certbot certonly \
  --webroot \
  -w /var/www/certbot \
  -d example.com \
  -d www.example.com

# Step 4: 更新 Nginx 配置为完整的 HTTPS 配置
# (替换为上面的 HTTPS 配置)

# Step 5: 重启 Nginx
docker compose restart nginx

# Step 6: 启动 Certbot 自动续期
docker compose up -d certbot

11.4 Nginx + Certbot 自动续期容器

使用 Shell 脚本实现续期循环

# Dockerfile.certbot
FROM certbot/certbot:latest

RUN apk add --no-cache bash

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
#!/bin/bash
# entrypoint.sh
# 描述:Certbot 容器入口脚本,实现定期续期

set -e

# 获取续期间隔(默认 12 小时)
RENEW_INTERVAL=${RENEW_INTERVAL:-43200}

echo "Certbot auto-renewal started"
echo "Renewal interval: ${RENEW_INTERVAL} seconds"

# 首次运行:检查并续期
certbot renew --quiet

# 循环续期
while true; do
    sleep "${RENEW_INTERVAL}" &
    wait ${!}
    echo "[$(date)] Running certificate renewal check..."
    certbot renew --quiet \
      --deploy-hook "echo 'Certificate renewed at $(date)' >> /var/log/letsencrypt/renewal.log"
done
# docker-compose.yml 片段
services:
  certbot:
    build:
      context: .
      dockerfile: Dockerfile.certbot
    environment:
      - RENEW_INTERVAL=43200  # 12 小时
    volumes:
      - certbot-certs:/etc/letsencrypt
      - certbot-webroot:/var/www/certbot
      - certbot-logs:/var/log/letsencrypt
    restart: unless-stopped

11.5 反向代理集成

场景:Nginx 反向代理多个后端服务

version: "3.8"

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-certs:/etc/letsencrypt:ro
      - certbot-webroot:/var/www/certbot:ro
    restart: unless-stopped

  certbot:
    image: certbot/certbot:latest
    volumes:
      - certbot-certs:/etc/letsencrypt
      - certbot-webroot:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done'"
    restart: unless-stopped

  app-frontend:
    image: node:alpine
    working_dir: /app
    command: npm start
    volumes:
      - ./frontend:/app
    expose:
      - "3000"
    restart: unless-stopped

  app-backend:
    image: python:3.11-slim
    working_dir: /app
    command: python app.py
    volumes:
      - ./backend:/app
    expose:
      - "8000"
    restart: unless-stopped

volumes:
  certbot-certs:
  certbot-webroot:

Nginx 反向代理配置

# ./nginx/conf.d/default.conf
upstream frontend {
    server app-frontend:3000;
}

upstream backend {
    server app-backend:8000;
}

server {
    listen 80;
    server_name example.com api.example.com;

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

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    location / {
        proxy_pass http://frontend;
        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 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    location / {
        proxy_pass http://backend;
        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;
    }
}

11.6 Traefik 集成

Traefik 是一个现代化的反向代理和负载均衡器,内置 ACME 支持,可以作为 Certbot 的替代方案。

Traefik + 自动 HTTPS

# docker-compose.yml
version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-certs:/letsencrypt
    restart: unless-stopped

  webapp:
    image: nginx:alpine
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.webapp.rule=Host(`example.com`)"
      - "traefik.http.routers.webapp.entrypoints=websecure"
      - "traefik.http.routers.webapp.tls.certresolver=letsencrypt"
    restart: unless-stopped

volumes:
  traefik-certs:

注意: 如果你已经熟悉 Certbot 并希望继续使用它,可以将 Traefik 仅作为反向代理,证书管理仍交给 Certbot。

11.7 Caddy 集成

Caddy 内置自动 HTTPS,也可以配合使用。

version: "3.8"

services:
  caddy:
    image: caddy:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    restart: unless-stopped

volumes:
  caddy-data:
  caddy-config:
# Caddyfile
example.com {
    reverse_proxy app:3000
}

11.8 Docker 环境续期策略

方案一:容器内循环续期(推荐)

services:
  certbot:
    image: certbot/certbot:latest
    volumes:
      - certbot-certs:/etc/letsencrypt
      - certbot-webroot:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done'"
    restart: unless-stopped

方案二:宿主机 cron 触发

# 宿主机 crontab
# 每天凌晨 2 点和下午 2 点执行续期
0 2,14 * * * docker exec certbot certbot renew --quiet && docker exec nginx nginx -s reload

方案三:systemd timer 触发

# /etc/systemd/system/certbot-docker.timer
[Unit]
Description=Certbot Docker renewal timer

[Timer]
OnCalendar=*-*-* 02,14:00:00
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
# /etc/systemd/system/certbot-docker.service
[Unit]
Description=Certbot Docker renewal service

[Service]
Type=oneshot
ExecStart=/usr/bin/docker exec certbot certbot renew --quiet
ExecStartPost=/usr/bin/docker exec nginx nginx -s reload
sudo systemctl daemon-reload
sudo systemctl enable certbot-docker.timer
sudo systemctl start certbot-docker.timer

11.9 Docker 安全注意事项

权限与安全性

# 只读挂载证书目录
services:
  nginx:
    volumes:
      - certbot-certs:/etc/letsencrypt:ro  # 只读挂载

网络隔离

networks:
  web:      # 前端网络(Nginx 对外)
    driver: bridge
  internal: # 内部网络(后端服务之间)
    driver: bridge
    internal: true

services:
  nginx:
    networks:
      - web
      - internal
  certbot:
    networks:
      - web    # 仅需要访问 Let's Encrypt 服务器
  app:
    networks:
      - internal  # 不直接暴露到外部

资源限制

services:
  certbot:
    image: certbot/certbot:latest
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.5"
    restart: unless-stopped

11.10 常见问题

问题 1:证书目录为空

# 确认卷正确挂载
docker compose exec nginx ls -la /etc/letsencrypt/live/

# 确认数据目录存在且有权限
ls -la /opt/certbot-deploy/data/certs/

问题 2:续期后 Nginx 未重载

# 方案 1:使用 deploy-hook
docker compose run --rm certbot renew \
  --deploy-hook "echo 'renewed'"

# 方案 2:宿主机 cron 同时重载 Nginx
# 见 11.8 方案二

问题 3:Webroot 验证失败

# 确认共享卷正确配置
docker compose exec nginx cat /var/www/certbot/.well-known/acme-challenge/test
# 确认 Nginx 配置中 location 正确
docker compose exec nginx nginx -T | grep acme

11.11 最佳实践

  1. 使用 Docker volume: 便于数据管理和备份
  2. 容器内循环续期: 使用 entrypoint 脚本实现自动续期
  3. 网络隔离: 将 Certbot 和 Web 服务器放在同一网络
  4. 只读挂载: Nginx 容器中证书目录使用只读挂载
  5. 日志持久化: 将日志挂载到宿主机,便于排查
  6. 健康检查: 添加 Docker health check 监控服务状态

扩展阅读