Контейнеризация и автоматизация веб-сайтов, или Docker + Nginx + LetsEncrypt
Цели которые мы реализуем в рамках статьи:
- использовать только OpenSource решения с неограниченной бесплатной лицензией для коммерческого использования
- использовать Docker-контейнеры как механизм технической и организационной архитектуры
- использовать Nginx как маршрутизатор и балансировщик HTTP-сервисов
- использовать LetsEncrypt для обеспечения бесплатного и автоматизированного получения и продления SSL сертификатов
- настроить Nginx на автоматическую динамическую настройку маршрутизации доменов при изменении состава активных Docker-контейнеров
Требования:
- в качестве ОС хост-машин используется Ubuntu 18.04 LTS на платформе x86_64 или amd64 (для других ОС могут потребоваться изменения в действиях раздела "Установка Docker", смотрите официальное руководство по установке)
Приступим!
Установка Docker
Для начала подготовим хост-машину к установке, для этого добавим ключ и репозиторий Docker:
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
Установим сам Docker Community Edition:
sudo apt-get update
sudo apt-get install docker-ce
Установка из репозитория позволяет не думать об обновлениях Docker, так как обновления производятся стандартными системными средствами.
После установки требуется произвести ещё пару настроек:
sudo groupadd docker
sudo usermod -aG docker $USER
Установка Docker-Compose
Для установки docker-compose требуется ещё меньше действий:
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Обновление docker-compose производится аналогичными командами с заменой номера версии в URL.
Создание отдельной внутренней сети
Для работы системы потребуется несколько служебных контейнеров, которые будут взаимосвязаны между собой и которые будут обмениваться настройками с подключаемыми HTTP-сервисами через отдельную виртуальную сеть, назовём эту сеть "nginx".
Для создания сети достаточно исполнить:
docker network create nginx
Создание структуры каталогов
Создадим каталог проекта:
mkdir nginx-stack
cd nginx-stack
Внутри каталога проекта создадим каталоги для SSL-сертификатов, настроек, лог-файлов и публичных файлов необходимых для подтвеждения сертификатов LetsEncrypt:
mkdir certs cfg log www
По необходимости создадим файлы для системы контроля версий, на примере Git:
git init
printf "*\n\!.gitignore" > certs/.gitignore
printf "" > cfg/.gitignore
printf "*\n\!.gitignore" > log/.gitignore
printf "" > www/.gitignore
Создание файла настроек для Nginx
Создадим файл nginx.conf в каталоге проекта и заполним следующими настройками:
user nginx;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 8192;
error_log /var/log/nginx/error.log crit;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
}
map $http_upgrade $proxy_connection {
default upgrade;
'' close;
}
map $scheme $proxy_x_forwarded_ssl {
default off;
https on;
}
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 off;
client_body_timeout 10;
default_type application/octet-stream;
keepalive_timeout 30;
keepalive_requests 1000;
reset_timedout_connection on;
send_timeout 2;
sendfile on;
server_names_hash_bucket_size 128;
server_tokens off;
tcp_nopush on;
tcp_nodelay on;
# Compression
gzip on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_disable "msie6";
# Caches information about open FDs, freqently accessed files
open_file_cache max=8192 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
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 $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header Proxy "";
include /etc/nginx/mime.types;
include /etc/nginx/cfg/*.conf;
}
При старте сервиса Nginx будет загружать файл настроек nginx.conf, а также mime.types и все файлы с расширением ".conf" из каталога cfg, в том числе automatic.conf, - автоматически генерируемый файл настроек.
В процессе работы каталог cfg может содержать:
- automatic.conf - автоматически сгенерированные конфигуратором настройки для всех найденных доменов, контейнеры которых активны в сети "nginx"
- default - автоматически сгенерированный letsencrypt-ом файл настроек для секции server, добавляемый каждому домену конфигуратором, в случае если нет файла специально под этот домен
- default.location - опционально, файл который добавляется конфигуратором в секцию location каждому домену, в случае если нет файла специально под этот домен
- опционально, файлы по имени домена, например, nixxlab.pro - содержимое файла добавляется конфигуратором в секцию server, заменяя собой default
- опционально, файлы по имени домена с добавочным расширением .location, например, nixxlab.pro.location - содержимое файла добавляется конфигуратором в секцию location, заменяя собой default.location
Создание файла настроек для Nginx-Configurator
Создадим файл configuration.template в каталоге проекта и заполним следующими настройками:
{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }}
{{ if (exists "/etc/nginx/certs/dhparam.pem") }}
ssl_dhparam /etc/nginx/certs/dhparam.pem;
{{ end }}
{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}
{{ $upstream := (first (groupByKeys $containers "Env.UPSTREAM")) }}
{{ $host := trim $host }}
{{ $is_regexp := hasPrefix "~" $host }}
{{ $upstream_name := when $is_regexp (sha1 $host) $host }}
{{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }}
{{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}}
{{ $vhostCert := trimSuffix ".crt" $vhostCert }}
{{ $vhostCert := trimSuffix ".key" $vhostCert }}
{{ $cert := (coalesce $certName $vhostCert) }}
upstream {{ $upstream_name }} {
server {{ $upstream }};
}
server {
listen 80;
listen [::]:80;
server_name {{ $host }};
{{ if (exists (printf "/etc/nginx/cfg/%s" $host)) }}
include {{ printf "/etc/nginx/cfg/%s" $host }};
{{ else if (exists "/etc/nginx/cfg/default") }}
include /etc/nginx/cfg/default;
{{ end }}
location / {
return 301 https://$host$request_uri;
}
}
{{ if (exists (printf "/etc/nginx/certs/%s.crt" $host)) }}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {{ $host }};
access_log /var/log/nginx/{{ $host }}.log main;
add_header Strict-Transport-Security "max-age=31536000";
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!DSS';
ssl_prefer_server_ciphers on;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }};
ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }};
{{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }}
ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }};
{{ end }}
{{ if (exists (printf "/etc/nginx/certs/%s.chain.crt" $cert)) }}
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate {{ printf "/etc/nginx/certs/%s.chain.crt" $cert }};
{{ end }}
{{ if (exists (printf "/etc/nginx/cfg/%s" $host)) }}
include {{ printf "/etc/nginx/cfg/%s" $host }};
{{ else if (exists "/etc/nginx/cfg/default") }}
include /etc/nginx/cfg/default;
{{ end }}
location / {
proxy_pass http://{{ trim $upstream_name }};
{{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
auth_basic "Restricted {{ $host }}";
auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }};
{{ end }}
{{ if (exists (printf "/etc/nginx/cfg/%s.location" $host)) }}
include {{ printf "/etc/nginx/cfg/%s.location" $host}};
{{ else if (exists "/etc/nginx/cfg/default.location") }}
include /etc/nginx/cfg/default.location;
{{ end }}
}
}
{{ end }}
{{ end }}
Создание правил для запуска стэка через Docker-Compose
Создадим файл docker-compose.yml в каталоге проекта и заполним следующими настройками:
version: '3'
networks:
nginx:
external: true
project:
driver: bridge
services:
######################################################################
nginx:
networks:
- nginx
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
volumes:
- ./log:/var/log/nginx:rw
- ./certs:/etc/nginx/certs:ro
- ./cfg:/etc/nginx/cfg:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./www:/usr/share/nginx/html:ro
container_name: nginx
image: nginx:1.13.1
labels:
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true"
restart: always
######################################################################
nginx-configurator:
networks:
- project
volumes:
- ./cfg:/etc/nginx/cfg:rw
- ./configuration.template:/etc/nginx/configuration.template:ro
- ./certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
container_name: nginx-configurator
image: jwilder/docker-gen:0.7.3
command: -notify-sighup nginx -watch -wait 5s:30s /etc/nginx/configuration.template /etc/nginx/cfg/automatic.conf
depends_on:
- nginx
restart: always
######################################################################
letsencrypt:
networks:
- project
volumes:
- ./certs:/etc/nginx/certs:rw
- ./cfg:/etc/nginx/vhost.d:rw
- ./www:/usr/share/nginx/html:rw
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
NGINX_PROXY_CONTAINER: nginx
NGINX_DOCKER_GEN_CONTAINER: nginx-configurator
container_name: letsencrypt
image: jrcs/letsencrypt-nginx-proxy-companion
depends_on:
- nginx
- nginx-configurator
restart: always
Запуск, останов и прочие нюансы
Для запуска стэка контейнеров достаточно использовать из каталога проекта:
docker-compose up -d
Для остановки:
docker-compose down
Для просмотра логов:
docker-compose logs -f
Для правильной работы конфигуратора необходимо сначала запускать описанный стэк контейнеров и лишь затем контейнеры с сайтами. Остановка производится в обратном порядке - сначала отключаются сайты, а затем
стэк описанных контейнеров.
На этом всё! Три настроенных и запущенных контейнера позволяют подхватывать включаемые контейнеры с сайтами и получать для них SSL-сертификаты в автоматическом режиме.