Получение и ротация SSL сертификатов с Certbot в Docker

Certbot не нужно держать как постоянный daemon. Запускаем one-shot контейнер по cron — меньше ресурсов, проще отлаживать.

Схема работы

  1. Nginx слушает порт 80, отдаёт ACME challenge из общей директории
  2. Certbot запускается через docker compose run --rm, проверяет/обновляет сертификат
  3. После обновления nginx перечитывает конфиг через SIGHUP
  4. Всё управляется одной cron-задачей

Подготовка docker-compose.yml

Certbot — сервис с профилем, не стартует при docker compose up:

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
 
  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    profiles:
      - certbot
  • profiles: [certbot] — контейнер не стартует автоматически, только через docker compose run --rm certbot или --profile certbot
  • Nginx монтирует certbot/conf как read-only (:ro), certbot — с записью
  • certbot/www — общая директория для ACME webroot challenge

Конфиг nginx для ACME challenge

server {
    listen 80;
    server_name example.com;
 
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
 
    location / {
        return 301 https://$host$request_uri;
    }
}

Первичное получение сертификата

SSL-сертификата ещё нет, HTTPS-блок nginx не заработает. Порядок:

1. Запустить временный nginx только с HTTP:

# Временный конфиг без HTTPS-блока
docker run -d --name nginx-init \
  -p 80:80 \
  -v ./nginx-init.conf:/etc/nginx/conf.d/default.conf:ro \
  -v ./certbot/www:/var/www/certbot:ro \
  nginx:1.27-alpine

2. Получить сертификат:

docker run --rm \
  -v ./certbot/conf:/etc/letsencrypt \
  -v ./certbot/www:/var/www/certbot \
  certbot/certbot:latest \
  certonly --webroot \
    --webroot-path=/var/www/certbot \
    --email your@email.com \
    --agree-tos \
    --no-eff-email \
    -d example.com

3. Убрать временный контейнер и запустить полный стек:

docker rm -f nginx-init
docker compose up -d

Автоматическая ротация

Скрипт обновления

/opt/project/ssl_renew.sh:

#!/bin/bash
cd /opt/project
 
docker compose run --rm certbot renew --no-random-sleep-on-renew \
  && docker compose kill -s SIGHUP nginx
  • run --rm — запустить, выполнить, удалить контейнер
  • certbot renew — обновляет только если до истечения менее 30 дней
  • --no-random-sleep-on-renew — без случайной задержки, мы и так по cron
  • SIGHUP nginxnginx перечитывает конфиг без downtime

Cron

# Ежедневно в 3:00 — проверка и обновление SSL
0 3 * * * /opt/project/ssl_renew.sh >> /opt/project/log/ssl_renew.log 2>&1

Certbot сам проверяет срок — если до истечения больше 30 дней, он ничего не делает. Безопасно запускать ежедневно.

Почему не daemon-контейнер

Часто встречается вариант с вечно работающим certbot:

# Не рекомендуется
entrypoint: "/bin/sh -c 'while :; do certbot renew; sleep 12h; done'"

Минусы: жрёт память постоянно (~50-100 MB), логи внутри контейнера, при падении sleep-цикла обновление тихо прекращается. One-shot через cron проще и надёжнее.

Связанные заметки