SSL сертификат для Docker web-app

    В данной статье я хочу поделиться с вами способом создания SSL сертификата для вашего веб-приложения работающего на Docker, т.к. в рускоязычной части интернета — подобного решения я не нашел.

    image

    Подробнее под катом.

    У нас был docker v.17.05, docker-compose v.1.21, Ubuntu Server 18 и пинта чистого Let'sEncrypt. Не то что бы обязательно нужно было разворачивать продакшен на Docker. Но если начал собирать Docker, становится трудно остановиться.

    Итак для начала приведу стандартные настройки — которые у нас были на этапе dev, т.е. без 443 порта и SSL в целом:

    docker-compose.yml
    version: '2'
    services:
        php:
            build: ./php-fpm
            volumes:
                - ./yourapp:/var/www/yourapp
                - ./php-fpm/php.ini:/usr/local/etc/php/php.ini
            depends_on:
                - mysql
            container_name: "yourapp"
        web:
            image: nginx:latest
            ports:
                - "80:80"
                - "443:443"
            volumes:
                - ./yourapp:/var/www/yourapp
                - ./nginx/main.conf:/etc/nginx/conf.d/default.conf
            depends_on:
                - php
        mysql:
            image: mysql:5.7
            command: mysqld --sql_mode=""
            environment:
                MYSQL_ROOT_PASSWORD: xxx
            ports:
                - "3333:3306"
    


    nginx/main.conf
     server {
        listen 80;
        server_name *.yourapp.ru yourapp.ru;
       root /var/www/yourapp/public;
         client_max_body_size 5M;
    
        location / {
            # try to serve file directly, fallback to index.php
            try_files $uri /index.php$is_args$args;
      }
    
        location ~ ^/index\.php(/|$) {
           fastcgi_pass php:9000;
           fastcgi_split_path_info ^(.+\.php)(/.*)$;
          include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
           fastcgi_param DOCUMENT_ROOT $realpath_root;
            fastcgi_buffer_size 128k;
           fastcgi_buffers 4 256k;
            fastcgi_busy_buffers_size 256k;
           internal;
        }
    
        location ~ \.php$ {
            return 404;
        }
    
         error_log /var/log/nginx/project_error.log;
        access_log /var/log/nginx/project_access.log;
    }
    


    Далее собственно нам необходимо реализовать SSL. Честно сказать — штудировал com зону я порядка часов 2-х. Все предложенные варианты там — интересные. Но на текушем этапе проекта, нам(бизнесу) нужно было быстро и надежно прикрутить SSL Let'sEnctypt к nginx контейнеру и не более.

    В первую очередь мы установили на сервер certbot
    sudo apt-get install certbot

    Далее мы сгенерили сертификаты wildcard для нашего домена

    sudo certbot certonly -d yourapp.ru -d *.yourapp.ru --manual --preferred-challenges dns

    после выполнения certbot предоставит нам 2 TXT записи, которые нужно указать в настройках DNS.

    _acme-challenge.yourapp.ru TXT {тотКлючКоторыйВамВыдалCertBot}

    И нажимаем enter.

    После этого certbot проверит наличие этих записей в DNS и создаст для вас сертификаты.
    если вы добавили сертификат, но certbot его не нашел — пробуйте перезапустить команду через 5-10 минут.

    Ну вот мы и гордые обладатели Let'sEncrypt сертификата на 90 дней, но теперь нам нужно его прокинуть в Docker.

    Для этого банальнейшим образом в docker-compose.yml, в секции nginx — прилинковываем дирректории.

    Пример docker-compose.yml с SSL
    version: '2'
    services:
        php:
            build: ./php-fpm
            volumes:
                - ./yourapp:/var/www/yourapp
                - /etc/letsencrypt/live/yourapp.ru/:/etc/letsencrypt/live/yourapp.ru/
                - ./php-fpm/php.ini:/usr/local/etc/php/php.ini
            depends_on:
                - mysql
            container_name: "yourappPHP"
        web:
            image: nginx:latest
            ports:
                - "80:80"
                - "443:443"
            volumes:
                - ./yourapp:/var/www/yourapp
                - /etc/letsencrypt/:/etc/letsencrypt/
                - ./nginx/main.conf:/etc/nginx/conf.d/default.conf
            depends_on:
                - php
        mysql:
            image: mysql:5.7
            command: mysqld --sql_mode=""
            environment:
                MYSQL_ROOT_PASSWORD: xxx
            ports:
                - "3333:3306"
    


    Прилинковали? Супер — продолжаем:

    Теперь нам нужно изменить конфиг nginx на работу с 443 портом и SSL в целом:

    Пример конфига main.conf с SSL
    #
    server {
    	listen 443 ssl http2;
    	listen [::]:443 ssl http2;
    
    	server_name *.yourapp.ru yourapp.ru;
    	set $base /var/www/yourapp;
    	root $base/public;
    
    	# SSL
    	ssl_certificate /etc/letsencrypt/live/yourapp.ru/fullchain.pem;
    	ssl_certificate_key /etc/letsencrypt/live/yourapp.ru/privkey.pem;
    	ssl_trusted_certificate /etc/letsencrypt/live/yourapp.ru/chain.pem;
    
          client_max_body_size 5M;
    
          location / {
              # try to serve file directly, fallback to index.php
              try_files $uri /index.php$is_args$args;
          }
    
          location ~ ^/index\.php(/|$) {
              #fastcgi_pass unix:/var/run/php7.2-fpm.sock;
              fastcgi_pass php:9000;
              fastcgi_split_path_info ^(.+\.php)(/.*)$;
              include fastcgi_params;
              fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
              fastcgi_param DOCUMENT_ROOT $realpath_root;
              fastcgi_buffer_size 128k;
              fastcgi_buffers 4 256k;
              fastcgi_busy_buffers_size 256k;
              internal;
          }
    
          location ~ \.php$ {
              return 404;
          }
    
          error_log /var/log/nginx/project_error.log;
          access_log /var/log/nginx/project_access.log;
    }
    
    
    # HTTP redirect
    server {
    	listen 80;
    	listen [::]:80;
    
    	server_name *.yourapp.ru yourapp.ru;
    
    	location / {
    		return 301 https://yourapp.ru$request_uri;
    	}
    }
    


    Собственно после этих манипуляций — идем в дирректорию с Docker-compose, пишем docker-compose up -d. И проверяем работоспособность SSL. Должно все взлететь.

    Главное не забывайте что Let'sEnctypt сертификат выдается на 90 дней и вам необходимо будет его обновить через команду sudo certbot renew, а после перезапустить проект командой docker-compose restart

    Как вариант — добавить данную последовательность в crontab.

    На мой взгляд это самый простой способ подключения SSL к Docker Web-app.

    P.S. Прошу принять во внимание что все скрипты представленные в тексте — не окончательны, сейчас проект находится на стадии глубокого Dev, по этому хочу вас попросить не критиковать конфиги- они будут еще много раз видоизменяться.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 21

      +8
      Самый простой имхо, это настроить Traefik.

      v1
      [acme]
      email = "mail@domain.ru"
      storage = "acme.json"
      caServer = "https://acme-v02.api.letsencrypt.org/directory"
      entryPoint = "https"
        [acme.httpChallenge]
        entryPoint = "http"
      
      [[acme.domains]]
      main = "domain.ru"
      


      v2
      [certificatesResolvers.sample.acme]
        caServer = "https://acme-v02.api.letsencrypt.org/directory"
        email = "mail@domain.ru"
        storage = "acme.json"
        [certificatesResolvers.sample.acme.httpChallenge]
          # used during the challenge
          entryPoint = "web"
      
          [http.routers.api]
            rule = "Host(`traefik.domain.ru`)"
            entrypoints = ["websecure"]
            service = "api@internal"
            middlewares = ["auth"]
            [http.routers.api.tls]
              certResolver = "sample"
      
        +1

        Согласен, если вы не Netflix — ставьте traefik и забудьте про остальное. Traefik ваш царь и бог в роутинге, SSL и проксировании

          0
          Можно какую-нибудь ссылку на клевый аргументированный обзор типа «nginx vs Traefik»? Почему второе, а не просто nginx?
            +1

            Потому что traefik разрабатывался как более дружелюбный к человеку прокси-сервер, чем nginx. Особенность traefik, что это именно ПРОКСИ, т.е. статику он отдавать не умеет. Ну, и то, что он всего лишь чутка медленнее nginx — никак не может быть аргументов в выборе в пользу nginx.

              +3

              Как сказал gecube — некорректно сравнивать. Вот haproxy VS traefik ещё можно сравнить)

            –3
            при чем тут Traefik ???

            Тема поста «SSL сертификат для Docker web-app»

            Ты б еще предложил балансер поднять, на нем терминейтить серты и дальше тупо через http ходить в контейнер

            Вобщем если нечего сказать по теме — лучше помолчать.
              +2

              А вместо того, чтобы накидывать агрессию — лучше было бы включить голову и узнать, что Traefik умеет внутри себя получать сертификаты LE и не нужно придумывать какие-то костыли. Я уж не говорю о том, что им очень удобно проксировать докер-контейнеры, т.к. он умеет определять их наличие по лейблам (меткам) и по ним же строить свою конфигурацию. Для тестовой среды почти идеально.

            0
            Это не тоже самое?
            awsswa.livejournal.com/41766.html
              +6
              Docker

              В первую очередь мы установили на сервер certbot


              *фейспалм* Кхм…

              А если по теме, может кто понятно объяснить, зачем вообще такое городить? Кажется очевидным, что обычная схема сервера выглядит как один nginx, установленный на сервере, смотрящий наружу и проксирующий все запросы дальше куда надо. Как там умными словами, SSL Terminator это вроде называется. Соответственно, все сертификаты кладутся туда. А ваше приложение из докера должно экспозить чистый HTTP порт и не заботиться о сертификатах вообще. Ваш докер-компоуз приложения даже не должен знать, какие где там сертификаты — это все разруливается на внешнем nginx. Зачем делать иначе?
                +2

                Вы про схему 2 nginx и fpm в одном docker-compose? Или про отдельный nginx со своим Докер-композит и демоном, который следит за докер-сокетом, перегенерирует конфиги nginx, если поднялся новый nginx с определёнными параметрами?


                Иногда мне кажется, что с разделением ответственностей и автоматизации люди перегибают. Поднимать 2 или даже 5 иногда nginx (tsl терминатор, роутер, генерируемая статика а-ля SPA приложения на реакт, пользовательские файлы, морда для fpm) для сайта-визитки или простого mvp какого-то…

                  0
                  Нет, что-то вы совсем страшное говорите.

                  Я про схему «один обычный nginx на сервере (не в докере), он же SSL Terminator, проксирующий запросы в нужные контейнеры» + все ваши докер-компоузы

                  Т.е. в случае простого веб-приложения с докер-компоузом из трех сервисов (входной nginx для вашего приложения, он же раздает статику; БД; контейнер, исполняющий логику (uWSGI в случае Питона и Django, например)) внешний nginx должен просто все запросы проксировать в ваш открытый порт, раскрывая SSL.

                  Итого для N приложений на одном сервере у вас 1 nginx + N компоузов. В каждом компоузе nginx, fpm/uwsgi/unicorn и MySQL. Единственный софт, который стоит на сервере — nginx, докер и компоуз. Все.
                    0

                    Да и вообще — выясняется, что на целевом сервере компоуз-то и не нужен. Во-первых, он прекрасно работает с удаленным докер демоном. А, во-вторых, вместо компоуза намного более веселее использовать ansible для управления контейнерами, т.к. те же миграции и очередность запуска контейнеров через компоуз очень плохо реализовывать.

                +1
                У девопсов реально всё так плохо с тем, чтобы написать удобочитаемый конфиг для nginx, или это локальная проблема автора статьи?
                  +1

                  А с чего Вы решили, что автор девопс? Если б он был девопсом, то знал, что


                  1. Есть образ jwilder/nginx, который умеет тоже самое и выписывать сертификаты автоматически. Ну, или traefik как выше вспомнили
                  2. Можно автоматизировать создание TXT-записи отделегировав или перенеся зону на Cloudflare, route53 или любой другой днс-хостинг с поддерживаемым certbot'ом api.

                  VolCh я могу представить зачем нужны пляски именно с https — т.к. хочется отлаживать веб-приложенте именно в той конфигурации, которая поедет на продакшн. Там много нюансов с cors, hsts, mixed content etc.

                    +1
                    «удобочитаемый» — это Вы про пробелы (отступы)?
                      +1
                      Да. КМК это настолько «детская болезнь», что видеть её в 2019-ом как-то странно.
                    0
                    а почему не используете hostname + docker dns (днс сервер в докере)? (если локально)

                    На проде использовал следующую схему:
                    — каждый микросервис (golang + mysql) в своем docker compose
                    — vps 1CPU + 1GB Ram (около 8ми микросервисов, пришлось мускл потюнить чтобы меньше в холостую озу жрал, с 150мб в пике получилось 57-60мб)
                    — nginx смотрит в мир, а микросервисы из докера прокинул через unix-socket (кроме мускл естественно)
                    — ssl получаю тем-же lets encrypt на 90дней + крон автопродлевает

                    профит:
                    — ssl-сертификат обновляется за 1 раз для всех микросервисов\хостов
                    — все настройки микросервисов (читай веб-контейнеров) в одной папке nginx на vps
                    — уменьшается к-тво контейнеров для управления

                    очень простая и удобная схема… до этого разные варианты перепробовал, вплоть до извращения:
                    — собираем образ ubuntu -> golang -> mysql -> nginx -> lets encrypt
                    — билдим, настраиваем докер файл
                    — 1 контейнер на микросервис
                    :)

                    и кстати, certbot иногда глюкает, и если слишком рано запустить обновление (до expire time) ничего не произойдет, хз почему так, устанавливался как с офф пакета, так и с репы.

                      +2

                      А мог поднять контейнер с traefik, прописать в docker-compose каждому микросервису labels с доменами и забыть про certbot, cron и извращения. Новые сертификаты выдаются автоматом на основе labels в docker-compose. Плюс у вас появляется UI со статистикой по запросам на микросервисы.

                        0
                        спасибо за фидбэк )
                        это было года полтора назад, если не два, делал первые шаги в использовании докера в проде,
                        таки да, здорово упрощает жизнь.
                      0
                      Спасибо за статью!

                      Подскажите пожалуйста, если после ввода комманды:

                      sudo certbot certonly -d {domen}.ru -d *.{domen}.ru -d {domen}.com -d *.{domen}.com --manual --preferred-challenges dns;

                      для доменов при генерации некоего домена указаны TXT записи типа:

                      _acme-challenge.{domen}.ru TXT {тотКлючКоторыйВамВыдалCertBot}

                      Добавить даннцю запись в DNS зону нужно прежде чем продолжить процесс формированиия сертификата? Вот кусок лога:

                      Please deploy a DNS TXT record under the name
                      _acme-challenge.{domen}.ru with the following value:

                      nG1_sgbfTM_SpB3lHN_n556R8Guu0l2n-jUhID98fIQ

                      Before continuing, verify the record is deployed.
                      (This must be set up in addition to the previous challenges; do not remove,
                      replace, or undo the previous challenge tasks yet. Note that you might be
                      asked to create multiple distinct TXT records with the same name. This is
                      permitted by DNS standards.)


                      После того, как я продолжил, словил:

                      Press Enter to Continue
                      Waiting for verification…
                      Cleaning up challenges
                      Failed authorization procedure. {domen}.com (dns-01): urn:ietf:params:acme:error:dns :: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.{domen}.com, {domen}.ru (dns-01): urn:ietf:params:acme:error:dns :: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.{domen}.ru, {domen}.com (dns-01): urn:ietf:params:acme:error:dns :: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.{domen}.com, {domen}.ru (dns-01): urn:ietf:params:acme:error:dns :: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.{domen}.ru

                      IMPORTANT NOTES:
                      — The following errors were reported by the server:

                      Domain: {domen}.com
                      Type: None
                      Detail: DNS problem: NXDOMAIN looking up TXT for
                      _acme-challenge.{domen}.com

                      Domain: {domen}.ru
                      Type: None
                      Detail: DNS problem: NXDOMAIN looking up TXT for
                      _acme-challenge.{domen}.ru

                      Domain: {domen}.com
                      Type: None
                      Detail: DNS problem: NXDOMAIN looking up TXT for
                      _acme-challenge.{domen}.com

                      Domain: {domen}.ru
                      Type: None
                      Detail: DNS problem: NXDOMAIN looking up TXT for


                      Может ли эту проблему исправить порядок действий, если сперва добавить TXT записи в DNS зону, а уже после продолжить процесс формирования сертификата?
                        0
                        Да, необходимо добавить запись до того как продолжить. В идеале подождать еще +- 5 минуток.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое