add: Nginx и Certboot в Docker (3)

This commit is contained in:
Sergei Erjemin 2025-02-14 02:16:18 +03:00
parent a149cab8c5
commit 6470803172

View File

@ -2,11 +2,21 @@
Для удобного переноса сайтов или веб-приложений между серверами, а также для упрощения обновления и обслуживания Для удобного переноса сайтов или веб-приложений между серверами, а также для упрощения обновления и обслуживания
веб-сервера nginx, удобно держать его в контейнере Docker. В данной инструкции рассмотрено развертывание веб-сервера веб-сервера nginx, удобно держать его в контейнере Docker. В данной инструкции рассмотрено развертывание веб-сервера
Nginx с SSL-сертификатами Let's Encrypt в контейнерах Docker. В качестве примера используется контейнер Portainer -- Nginx с SSL-сертификатами Let's Encrypt в контейнерах Docker. В качестве примера сайта используется контейнер Portainer --
отличный инструмент для управления Docker-контейнерами через веб-интерфейс. отличный инструмент для управления Docker-контейнерами через веб-интерфейс.
Соглашения: пусть наш пользователь от имени которого мы работаем -- `web`. Таким образом, домашний каталог -- `/home/web`. ### Соглашения:
Каталог для хранения данных Docker-контейнеров (место куда монтируют тома контейнера) -- `/home/web/docker-data`. | | |
|-----------------------------|----------------------------------------------------------------------------------------|
| `web` | Пользователь от имени которого мы работаем |
| `/home/web` | Домашний каталог. |
| `/home/web/docker-data` | Каталог для хранения данных Docker-контейнеров (место куда монтируют тома контейнеров) |
| `portainer.you.domain.name` | Домен, по которому будет доступен Portainer |
| `email@you.domain.name` | Email для сертификатов Let's Encrypt | |
Чтобы не случилось путаницы во время копи-паста рекомендуется сразу произвести все замены в тексте.
## Portainer + nginx в контейнерах Docker
И так, для начала создадим каталог для хранения данных Portainer (можно опустить если вам не нужен Portainer): И так, для начала создадим каталог для хранения данных Portainer (можно опустить если вам не нужен Portainer):
```bash ```bash
@ -43,14 +53,14 @@ server {
``` ```
Что происходит в этом файле конфигурации: Что происходит в этом файле конфигурации:
- `listen 80;` -- слушаем порт 80 (обычный HTTP-трафик); - `listen 80;` слушаем порт 80 (обычный HTTP-трафик);
- `server_name portainer.you.domain.name;` -- имя хоста, по которому будет доступен Portainer (замените `you.domain.name` - `server_name portainer.you.domain.name;` имя хоста, по которому будет доступен Portainer (замените `you.domain.name`
на ваш домен); на ваш домен);
- `proxy_pass http://portainer:9000;` -- проксируем запросы в контейнер Portainer, который будет доступен по хосту (имени - `proxy_pass http://portainer:9000;` проксируем запросы в контейнер Portainer, который будет доступен по хосту (имени
контейнера `portainer`) и порту 9000; контейнера `portainer`) и порту 9000;
- `proxy_set_header Host $host;` -- передаем заголовок `Host` в запросе; - `proxy_set_header Host $host;` передаем заголовок `Host` в запросе;
- `proxy_set_header X-Real-IP $remote_addr;` -- передаем заголовок `X-Real-IP` в запросе; - `proxy_set_header X-Real-IP $remote_addr;` передаем заголовок `X-Real-IP` в запросе;
- `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;` -- передаем заголовок `X-Forwarded-For` в запросе; - `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;` передаем заголовок `X-Forwarded-For` в запросе;
Сохраните файл и выйдите из редактора (`Ctrl + X`, затем `Y` для подтверждения). Сохраните файл и выйдите из редактора (`Ctrl + X`, затем `Y` для подтверждения).
@ -101,24 +111,24 @@ networks:
Что у нас настроено в этом `docker-compose.yml`: Что у нас настроено в этом `docker-compose.yml`:
- `portainer` (у вас его может и не быть, или быть какой-то другой сервис, который вы будете производить): - `portainer` (у вас его может и не быть, или быть какой-то другой сервис, который вы будете производить):
- `image: portainer/portainer-ce:latest` -- используем образ Portainer Community Edition; - `image: portainer/portainer-ce:latest` используем образ Portainer Community Edition;
- `container_name: portainer` -- имя контейнера `portainer`; - `container_name: portainer` имя контейнера `portainer`;
- `volumes: ...` -- монтируем файлы или тома изнутри контейнера Portainer на хост. В данном случае монтируем: сокет -- - `volumes: ...` монтируем файлы или тома изнутри контейнера Portainer на хост. В данном случае монтируем: сокет --
чтобы изнутри контейнера Portainer можно было управлять Docker, в котором сам же и работает (вот так хитро) и каталог чтобы изнутри контейнера Portainer можно было управлять Docker, в котором сам же и работает (вот так хитро) и каталог
`/home/web/docker-data/portainer` -- чтобы сохранять данные Portainer между перезапусками; `/home/web/docker-data/portainer` чтобы сохранять данные Portainer между перезапусками;
- `restart: always` -- автоматически перезапускаем контейнер при его остановке; - `restart: always` автоматически перезапускаем контейнер при его остановке;
- `networks: ...` -- подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`. - `networks: ...` подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`.
- `nginx`: - `nginx`:
- `image: nginx:latest` -- используем образ Nginx; - `image: nginx:latest` используем образ Nginx;
- `container_name: nginx` -- имя контейнера `nginx`; - `container_name: nginx` имя контейнера `nginx`;
- `ports: ...` -- пробрасываем порты 80 и 443 на хост; - `ports: ...` пробрасываем порты 80 и 443 на хост;
- `volumes: ...` -- монтируем каталог с конфигурационными файлами Nginx; - `volumes: ...` монтируем каталог с конфигурационными файлами Nginx;
- `restart: always` -- автоматически перезапускаем контейнер при его остановке; - `restart: always` автоматически перезапускаем контейнер при его остановке;
- `networks: ...` -- подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`. - `networks: ...` подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`.
- `networks: ...`: - `networks: ...`:
- `web`: - `web`:
- `driver: bridge` -- используем драйвер сети `bridge` (по умолчанию); - `driver: bridge` используем драйвер сети `bridge` (по умолчанию);
- `ipam: ...` -- настраиваем IP-адреса для контейнеров внутри сети `web`. В данном случае используем подсеть - `ipam: ...` настраиваем IP-адреса для контейнеров внутри сети `web`. В данном случае используем подсеть
Сохраняем файл `docker-compose.yml` и выходим из редактора (`Ctrl + X`, затем `Y` для подтверждения). Сохраняем файл `docker-compose.yml` и выходим из редактора (`Ctrl + X`, затем `Y` для подтверждения).
@ -132,7 +142,7 @@ docker-compose up -d
## Let's Encrypt в контейнере Docker ## Let's Encrypt в контейнере Docker
Создадим каталог для хранения ключей сертификатов Let's Encrypt: Создадим каталог для хранения ключей сертификатов Let's Encrypt и временных файлов для проверки владения доменом:
```bash ```bash
mkdir -p /home/web/docker-data/letsencrypt mkdir -p /home/web/docker-data/letsencrypt
mkdir -p /home/web/docker-data/letsencrypt/_cert mkdir -p /home/web/docker-data/letsencrypt/_cert
@ -157,7 +167,7 @@ mkdir -p /home/web/docker-data/letsencrypt/_ownership_check
*Что тут происходит и зачем нам такие мапинги томов?* *Что тут происходит и зачем нам такие мапинги томов?*
1. Когда certbot запрашивает сертификат у Let's Encrypt, то тот требует подтверждения владения доменом. При работе 1. Когда certbot запрашивает сертификат у Let's Encrypt, то тот требует подтверждения владения доменом. При работе
certbot в контейнере самый популярный (и лучший при работе в контейнере) способ подтверждения -- это HTTP-проверка. certbot в контейнере самый популярный (и лучший при работе в контейнере) способ подтверждения это HTTP-проверка.
Certbot создает временные файлы в каталоге `/var/www/html/letsencrypt` (мы будем указывать этот каталог при инициализации Certbot создает временные файлы в каталоге `/var/www/html/letsencrypt` (мы будем указывать этот каталог при инициализации
сертификата). Сервер же Let's Encrypt перед выдачей сертификата делает HTTP-запрос к этому временному файлу по URL сертификата). Сервер же Let's Encrypt перед выдачей сертификата делает HTTP-запрос к этому временному файлу по URL
(например, в нашем случае по `http://portainer.you.domain.namey/.well-known/acme-challenge/`) и если файл доступен, (например, в нашем случае по `http://portainer.you.domain.namey/.well-known/acme-challenge/`) и если файл доступен,
@ -178,10 +188,10 @@ mkdir -p /home/web/docker-data/letsencrypt/_ownership_check
можно не делать, и обойтись хуками certbot, но это сложнее. можно не делать, и обойтись хуками certbot, но это сложнее.
4. `entrypoint: ...`: 4. `entrypoint: ...`:
- `apk add --no-cache curl` -- устанавливаем пакет `curl`, который потребуется для работы с Docker API через сокет; - `apk add --no-cache curl` устанавливаем пакет `curl`, который потребуется для работы с Docker API через сокет;
- `trap exit TERM;` -- устанавливаем обработчик сигнала `TERM`, чтобы контейнер certbot корректно завершал работу; - `trap exit TERM;` устанавливаем обработчик сигнала `TERM`, чтобы контейнер certbot корректно завершал работу;
- `while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done` - `while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done`
-- это скрипт, который запускается при старте контейнера certbot. Он запускает certbot в режиме `renew` каждые 12 часов. это скрипт, который запускается при старте контейнера certbot. Он запускает certbot в режиме `renew` каждые 12 часов.
Таким образом, сертификаты будут автоматически обновляться каждые 12 часов. Таким образом, сертификаты будут автоматически обновляться каждые 12 часов.
@ -207,9 +217,9 @@ mkdir -p /home/web/docker-data/letsencrypt/_ownership_check
владения, но теперь для контейнера nginx*: владения, но теперь для контейнера nginx*:
- `volumes: ...`: - `volumes: ...`:
- `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` -- маппинг тома для сертификатов Let's Encrypt, чтобы их - `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` маппинг тома для сертификатов Let's Encrypt, чтобы их
можно было использовать в контейнере nginx; можно было использовать в контейнере nginx;
- `/home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt` -- маппинг тома временных файлов для - `/home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt` маппинг тома временных файлов для
проверки владения доменом со стороны Let's Encrypt. проверки владения доменом со стороны Let's Encrypt.
Сохраняем файл `docker-compose.yml`. Сохраняем файл `docker-compose.yml`.
@ -332,15 +342,15 @@ server {
Что изменилось в конфигурации: Что изменилось в конфигурации:
- Добавлены директивы для SSL для сервера в котором живет прокси на Portainer: - Добавлены директивы для SSL для сервера в котором живет прокси на Portainer:
- `listen 443 ssl;` -- теперь сервер слушает порт 443 (HTTPS) и использует SSL; - `listen 443 ssl;` теперь сервер слушает порт 443 (HTTPS) и использует SSL;
- `ssl_certificate ...` и `ssl_certificate_key ...` -- указываем пути к сертификату и ключу сертификата Let's Encrypt; - `ssl_certificate ...` и `ssl_certificate_key ...` указываем пути к сертификату и ключу сертификата Let's Encrypt;
- `ssl_protocols TLSv1.2 TLSv1.3;` -- указываем протоколы SSL/TLS, которые будут использоваться; - `ssl_protocols TLSv1.2 TLSv1.3;` указываем протоколы SSL/TLS, которые будут использоваться;
- `ssl_ciphers HIGH:!aNULL:!MD5;` -- указываем шифры, которые будут использоваться (`HIGH` → Разрешает только сильные - `ssl_ciphers HIGH:!aNULL:!MD5;` указываем шифры, которые будут использоваться (`HIGH` → Разрешает только сильные
шифры, например, AES256, `!aNULL` → Запрещает анонимные шифры, которые не используют аутентификацию и уязвимы шифры, например, AES256, `!aNULL` → Запрещает анонимные шифры, которые не используют аутентификацию и уязвимы
к MITM-атакам, `!MD5` → Запрещает использование хэша MD5, так как он давно признан небезопасным; к MITM-атакам, `!MD5` → Запрещает использование хэша MD5, так как он давно признан небезопасным;
- `ssl_prefer_server_ciphers on;` -- указываем, что сервер предпочтет использовать свои шифры, а не клиентские. - `ssl_prefer_server_ciphers on;` указываем, что сервер предпочтет использовать свои шифры, а не клиентские.
- Добавлен блок `server` для перенаправления с HTTP на HTTPS: - Добавлен блок `server` для перенаправления с HTTP на HTTPS:
- `return 301 https://$host$request_uri;` -- перенаправляем все запросы с порта 80 на порт 443 c кодом 301 (перемещено - `return 301 https://$host$request_uri;` перенаправляем все запросы с порта 80 на порт 443 c кодом 301 (перемещено
навсегда). навсегда).
Сохраним файл и, наконец запустим все контейнеры: Сохраним файл и, наконец запустим все контейнеры:
@ -380,7 +390,7 @@ Congratulations, all simulated renewals succeeded:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
``` ```
И наконец, при обновлении сертификатов нужно перезапускать контейнер nginx чтобы он перподключил эти сертификаты. И наконец, при обновлении сертификатов нужно перезапускать контейнер `nginx` чтобы он перподключил новые сертификаты.
Изнутри контейнера `letsencrypt-certbot` мы не можем управлять контейнерами Docker (и даже ничего не знаем о них), Изнутри контейнера `letsencrypt-certbot` мы не можем управлять контейнерами Docker (и даже ничего не знаем о них),
но можно добавить **хук** в `letsencrypt-certbot`, который будет перезапускать контейнер `nginx` сразу после успешного но можно добавить **хук** в `letsencrypt-certbot`, который будет перезапускать контейнер `nginx` сразу после успешного
обновления сертификатов (и только если обновление прошло успешно)! обновления сертификатов (и только если обновление прошло успешно)!