# Общие сведения > English version: [README.md](README.md) [Certbot](https://certbot.eff.org/) — клиент [Let's Encrypt](https://letsencrypt.org/) от EFF: получает и перевыпускает бесплатные TLS-сертификаты. Certbot, работающий в docker-контейнере циклом перевыпуска, в паре с контейнером [haproxy](https://git.bitdeals.org/private/haproxy), который он снабжает сертификатом. Репозиторий описывает только развёртывание в docker. # Использование Контейнер — не разовая команда. Его команда по умолчанию представляет собой цикл: запустить скрипт перевыпуска, поспать 12 часов и повторить, — поэтому сертификат он получает при первом старте и дальше поддерживает свежим. Каждый проход делает четыре вещи, по скрипту на каждую: |Скрипт|Что делает| |:--|:--| |`entrypoint.sh`|Сам цикл и PID 1 контейнера. Выполняет проход, спит 12 часов, повторяет; неудачный проход не обрывает цикл, а сообщается и повторяется| |`1-renew-cert.sh`|Начало прохода. Перевыпускает существующий сертификат либо передаёт управление `0-create-cert.sh`, если сертификата ещё нет| |`0-create-cert.sh`|Первый запуск: пишет самоподписанную заглушку, чтобы HAProxy смог занять 443, дожидается HAProxy и запрашивает настоящий сертификат| |`2-concatenate-cert.sh`|Склеивает `fullchain.pem` и `privkey.pem` в единый `site.pem`, которого ждёт HAProxy| |`3-update-haproxy-cert.sh`|Устанавливает `site.pem` в *работающий* HAProxy через его runtime API — без перезапуска и без разрыва соединений| Проверка владения — **HTTP-01 на порту 380**. Внутри контейнера на нём слушает собственный standalone-сервер certbot, а HAProxy передаёт туда `/.well-known/acme-challenge/` с публичного порта 80. Больше до 380 никто доступа иметь не должен. Let's Encrypt требует, чтобы публичные записи A/AAAA домена указывали на эту машину, а порт 80 был доступен из интернета. ## docker-compose ```yaml services: certbot: build: context: https://git.bitdeals.org/private/certbot.git dockerfile: ./docker/Dockerfile image: registry.bitdeals.org/certbot restart: unless-stopped environment: - CERTBOT_DOMAIN=example.org - CERTBOT_EMAIL=admin@example.org # необязательно, для уведомлений об истечении volumes: - certificates:/etc/certificates # общий с haproxy - letsencrypt:/etc/letsencrypt # ключ учётной записи, сертификаты, настройки перевыпуска - letsencrypt_work:/var/lib/letsencrypt volumes: certificates: letsencrypt: letsencrypt_work: ``` `certificates` — тот же том, который HAProxy подключает только на чтение; пишет в него именно этот контейнер. ## docker cli ```sh docker run -d \ -e CERTBOT_DOMAIN=example.org \ -v certificates:/etc/certificates \ -v letsencrypt:/etc/letsencrypt \ -v letsencrypt_work:/var/lib/letsencrypt \ registry.bitdeals.org/certbot ``` Цикл перевыпуска — это `CMD` образа, а точка входа базового образа (`certbot`) сброшена, поэтому всё, что указано после имени образа, заменяет цикл целиком: разовая команда над тем же состоянием не требует `--entrypoint`: ```sh docker run --rm \ -v letsencrypt:/etc/letsencrypt \ -v letsencrypt_work:/var/lib/letsencrypt \ registry.bitdeals.org/certbot certbot certificates ``` ## Сборка и публикация Push в `main` собирает и публикует образ (`.gitea/workflows/build.yaml`) с тремя тегами: `<версия>.` — для развёртывания, `<версия>` — для чтения и `latest` — для compose и Watchtower. Еженедельный cron пересобирает образ из тех же исходников. Вручную, если под рукой учётные данные реестра: ```sh docker build . --file docker/Dockerfile --tag registry.bitdeals.org/certbot docker push registry.bitdeals.org/certbot ``` **Контекст сборки — корень репозитория**, а не `docker/`: Dockerfile копирует `./docker/scripts/`, поэтому при контексте `./docker` этот каталог не виден и сборка падает на `COPY`. # Параметры Образы контейнера настраиваются параметрами, передаваемыми при запуске. |Параметр|Назначение| |:--------|:-------| |-e CERTBOT_DOMAIN|Домен, на который выпускается сертификат. По умолчанию: пусто — сертификат не запрашивается и сайт молча остаётся с самоподписанной заглушкой. Домен только один: скрипты передают единственный `-d`| |-e CERTBOT_EMAIL|Адрес для уведомлений Let's Encrypt об истечении срока. По умолчанию: пусто, регистрация идёт с `--register-unsafely-without-email`, и предупреждений не будет — см. «Замечания»| |-v /etc/certificates|Общий с HAProxy. Содержит `site.pem` — склеенные сертификат и приватный ключ, которые загружает HAProxy| |-v /etc/letsencrypt|Каталог конфигурации certbot: ключ учётной записи ACME, выпущенные сертификаты и настройки перевыпуска. Его потеря означает повторную регистрацию и повторный выпуск| |-v /var/lib/letsencrypt|Рабочий каталог certbot. Базовый образ объявляет его как `VOLUME`, поэтому без именованного тома при каждом создании контейнера возникает новый анонимный| |-p 380|Порт ACME-проверки HTTP-01. Внутренний: на него проксирует HAProxy. Публиковать его на хост не нужно и не следует| # Замечания - **Без адреса электронной почты предупреждений об истечении не будет.** Пустой `CERTBOT_EMAIL` означает регистрацию с `--register-unsafely-without-email`: если перевыпуск начнёт падать, вы узнаете об этом только когда сертификат истечёт. Следите за сертификатом внешними средствами или задайте переменную. - **Первый сертификат самоподписанный, и браузеры об этом скажут.** HAProxy не стартует без `site.pem`, поэтому `0-create-cert.sh` прежде всего пишет заглушку. Она заменяется, как только выпущен настоящий сертификат, — но если выпуск не удался, сайт продолжает отдавать именно заглушку, и сообщение об этом есть только в журнале контейнера. - **Приватный ключ попадает к HAProxy через unix-сокет, а не по сети.** `3-update-haproxy-cert.sh` передаёт весь `site.pem` в runtime API HAProxy — канал уровня `admin` без аутентификации, — поэтому том с `admin.sock` должен быть разделён с haproxy и больше ни с кем. TCP-порт был бы доступен любому контейнеру в общей сети, а правил по портам у docker-сетей нет. См. замечание о runtime API в [README haproxy](https://git.bitdeals.org/private/haproxy). - **Отказ в установке сообщается, а не проглатывается.** Runtime API сообщает об отказе текстом ответа и всё равно закрывает соединение штатно, поэтому код возврата socat ни о чём не говорит; `3-update-haproxy-cert.sh` вместо этого разбирает ответы на `set ssl cert` и `commit ssl cert` и останавливается на первом, который не является подтверждением. Исправить он ничего не может: `site.pem` на томе в любом случае верен, поэтому отказ означает, что *работающий* HAProxy остаётся на прежнем сертификате до перезапуска. Об этом он и пишет. - **Установка выполняется на каждом проходе, а не при перевыпуске.** Скрипт склеивает и переустанавливает сертификат независимо от того, сделал ли `certbot renew` хоть что-нибудь, — дважды в сутки. Вреда нет, но это значит, что путь «сертификат обновлён» задействуется постоянно и настоящий перевыпуск ничем не отличается от любого другого прохода. Для этого у certbot есть `--deploy-hook`. - **`site.pem` всегда заменяется атомарным переименованием** — и на пути заглушки, и на пути перевыпуска. HAProxy читает этот файл при старте, а обычное перенаправление вывода в него оставляет промежуток, в котором на томе лежит обрезанный PEM, — то есть сертификат, с которым HAProxy откажется стартовать. - **Остановка во время выпуска может не уложиться в отведённое docker время.** POSIX-оболочка выполняет обработчик сигнала только после возврата команды переднего плана, поэтому SIGTERM, пришедший, пока `certbot certonly` общается с Let's Encrypt, придерживается до конца этого вызова — дольше, чем отведённые `docker stop` по умолчанию 10 секунд, после которых контейнер убивают посреди выпуска. Ничего при этом не портится (состояние в `/etc/letsencrypt` переживает, следующий проход доделает), но если чистая остановка важна — задайте сервису `stop_grace_period: 60s`. Сон между проходами прерываемый и реагирует за миллисекунды. - **Скрипты подключаются через `.`, а не запускаются отдельным процессом**, поэтому текущий каталог, `set -e` и всякая оставленная переменная наследуются следующим скриптом. Именно поэтому они обращаются к файлам по абсолютным путям, а `cd` и `umask` держат внутри подоболочек; новые пишите по тому же правилу. - **Контейнер работает от root**, как и базовый образ, — ему нужно писать в `/etc/letsencrypt`. Прав здесь никто потом не понижает. - **Базовый образ не зафиксирован.** `FROM certbot/certbot:latest` вместе с еженедельной пересборкой по cron означает, что новый выпуск certbot попадает в реестр, а через Watchtower и в продуктив, без того чтобы кто-либо запускал сборку. Для воспроизводимых сборок фиксируйте версию тегом. - **У Let's Encrypt есть ограничения частоты.** Повторяющиеся неудачные попытки выпуска на один домен в них засчитываются; проверяйте изменения на `--server https://acme-staging-v02.api.letsencrypt.org/directory`, прежде чем оставлять цикл повторять их каждые 12 часов.