image


Ранее (11 февраля 2019) пост уже публиковался мной с таким заголовком, но был отправлен в небытие по причине смерти домена, а как следствие и сервиса с ним связанного. Причин тогда было две — статистика скачивания докер-образа оставляла желать лучшего, и цена за продление домена (что был зарегистрирован в nic) стала для меня неожиданно выше той, которую я был морально готов заплатить за него.


Но есть время не продлять домены разбрасывать камни, а есть время извиниться за сделанную ошибку. Все, кому сервис был полезен, кто им пользовался и однажды заметил что он (localhost.tools) не резолвится — приношу свои извинения, и исправляю ситуацию (лучше поздно чем никогда, верно?).


Ниже я вкратце расскажу о чем вообще идёт речь, что изменилось, и как этим всем пользоваться. Для нетерпеливых, традиционно — репозиторий и ссылочка на сайт.


Итак, докер. В личном списке технологий, что изменили индустрию в лучшую сторону — именно его бы поставил, пожалуй, на первое место. Сборка и запуск сложных сервисов (с чертовой дюжиной зависимостей в виде баз данных, коллекторов и прочего) используя его сводится к одной/двум командам, и это ли не прекрасно? Про деплой я вообще помалкиваю — красота, да и только (естественно что есть нюансы, но сейчас не о них).


Но вместе с тем, при локальной разработке порою возникает неудобство (именно неудобство, не блокер) в виде "разведения" сервисов по различным портам. И это работает здорово, пока у вас не появляется целый зоопарк приложений, переключение между которыми начинает вызывать некоторые неудобства, так как надо помнить и схему, и порт, и где-то фиксировать какие порты для какого приложения вы когда-то выделили, дабы не возникло коллизии со временем.


А потом ещё тебе хочется проверить работу используя https — и приходится либо использовать свой корневой сертификат (mkcert), либо всегда использовать curl --insecure ..., а когда над приложениями работают различные команды — количество запар начинает возрастать в прогрессии, и порою геометрической.


Столкнувшись с такой проблемой в очередной раз — в голове промелькнула мысль "хватит это терпеть!", и результатом работы на паре выходных стал сервис, который решает это неудобство, о чем будет рассказано ниже.


Мир Нас спасёт реверс-прокси


По-хорошему, нам нужна какая-то доменная зона, все под-домены из которой всегда будут резольвить локалхост (127.0.0.1). Беглые поиски навели на домены вида *.localho.st, *.lvh.me и другие, но как к ним прикрутить валидный SSL сертификат? Повозившись со своим корневым сертификатом удалось завести curl без ошибок, но не все браузеры корректно принимали его, и продолжали вываливать ошибку. Кроме того — крайне не хотелось всей этой "возни" с SSL.


"Что ж, зайдём с другой стороны!" — мной был приобретен домен с именем indocker.app, делегирован на CloudFlare, настроен требуемый резольвинг (все под-домены резольвят 127.0.0.1):


$ dig +noall +answer -t A foo.indocker.app # IPv4
foo.indocker.app.   7131    IN  A   127.0.0.1

$ dig +noall +answer -t AAAA foo.indocker.app # IPv6
foo.indocker.app.   86400   IN  AAAA    ::1

$ dig +noall +answer foo.bar.baz.indocker.app # any depth
foo.bar.baz.indocker.app. 86400 IN  A   127.0.0.1

После этого был бережно взял certbot, который используя DNS верификацию и API ключ от доменной зоны выдал мне TLS сертификат на зону *.indocker.app (если что, в конце поста есть ссылка на репозиторий с исходниками, и там подробно описано как именно это делается).


Теперь у нас есть и валидный TLS сертификат (пускай и на 3 месяца, и только для под-доменов одного уровня). Остается как-то научиться проксировать все запросы, что приходят на локалхост в нужный контейнер.


И тут на сцену врывается Traefik (спойлер — он прекрасен). Запустив его локально, и пробросив в его контейнер докер-сокет — он умеет проксировать запросы в тот контейнер, у которого есть необходимый docker label. Таким образом, нам нет необходимости в какой-либо дополнительной конфигурации, кроме как при запуске указать нужный label у контейнера, к которому мы хотим получить доступ по доменному имени!


Таким образом у нас появляется артефакт в виде докер-образа с пред-настроенным Traefik-ом и wildcard SSL сертификатом (да, он публичный).


Приватный ключ от SSL в публичном образе?


Да, но я считаю что это не страшно, так как он на доменную зону, которая всегда резольвит локалхост. MitM в этом случае не имеет особого смысла в принципе.


Что делать, когда сертификат протухнет?


Просто стянуть свежий образ, перезапустив контейнер. У проекта настроен CI, который в автоматическом режиме, раз в месяц (на данный момент) обновляет сертификат и публикует свежий образ.


Хочу попробовать!


Нет ничего проще. Первым делом, убедись что локальные порты 80 и 443 у тебя свободны, и выполни:


# Создаём docker-сеть для нашего реверс-прокси
$ docker network create indocker-app-network

$ docker run -d --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -p 80:80 -p 443:443 \
  --network indocker-app-network \
  --name indocker.app \
  quay.io/indocker/app:1 # 1 тут - это мажорное значение версии

И теперь проверки ради создадим простой docker-compose файл, и запустим сервисы в нём описанные:


version: '3.8'

services:
  my-nginx:
    image: nginx:latest
    labels:
      - traefik.enable=true
      # Router docs: https://doc.traefik.io/traefik/routing/providers/docker/#routers
      - traefik.http.routers.my-nginx-router.rule=Host(`my-nginx.indocker.app`)
      - traefik.http.routers.my-nginx-router.service=my-nginx-service
      # Service docs: https://doc.traefik.io/traefik/routing/providers/docker/#services
      - traefik.http.services.my-nginx-service.loadbalancer.server.port=80
      - traefik.http.services.my-nginx-service.loadbalancer.healthcheck.path=/
      - traefik.http.services.my-nginx-service.loadbalancer.healthcheck.interval=5s
    networks: [indocker-app-network]
    security_opt: [no-new-privileges:true]

  whoami:
    image: containous/whoami:latest
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami-router.rule=Host(`whoami.indocker.app`)
      - traefik.http.routers.whoami-router.entrypoints=http # force HTTP instead of HTTPS
      - traefik.http.routers.whoami-router.service=whoami-service
      - traefik.http.services.whoami-service.loadbalancer.server.port=8080
    command: --port 8080
    networks: [indocker-app-network]
    security_opt: [no-new-privileges:true]

networks:
  indocker-app-network:
    external: true

$ docker-compose up -d

И теперь можем потестировать:


$ curl -sS https://my-nginx.indocker.app | grep Welcome
<title>Welcome to nginx!</title>
<h1>Welcome to nginx!</h1>

$ curl -sS http://whoami.indocker.app
Hostname: 849834ab3299
IP: 127.0.0.1
IP: 172.20.0.3
RemoteAddr: 172.20.0.2:33734
GET / HTTP/1.1
Host: whoami.indocker.app
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
Accept: */*
Accept-Encoding: gzip
Referer:
X-Forwarded-For: 172.20.0.1
X-Forwarded-Host: whoami.indocker.app
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: edc8405eccc6
X-Real-Ip: 172.20.0.1

Как видим — работает :)


Так как домены в зоне .app находятся в HSTS preload list — при попытке открытия браузером любого поддомена используя http — запрос будет автоматически перенаправлен на схему https. Касается это только браузеров.

Кроме того, можем стукнуться браузером на адрес monitor.indocker.app, и увидеть дашборд локально запущенного Traefik:



Где живёт документация, описание?


Всё, как не сложно догадаться, живёт по адресу indocker.app. Более того, морда — отзывчивая, и умеет смотреть запущен ли у тебя локально демон. Все исходники живут в этом репозитории — если у кого-то появится желание сделать нечто похожее, но со своими особенностями — дерзайте!


Сколько стоит?


Нисколько. Совсем. Сделав данную штуку для себя и своей команды, пришло понимание того, что она может пригодиться и другим разработчикам. Более того — денег стоит только доменное имя, всё остальное — юзается без необходимости в оплате.


А в чем отличие от localhost.tools, который был сдохнут?


Во-первых — это домен. Он более привлекателен (как мне кажется), и приобретен не у российского регистратора (что важно). На момент написания этих строк он оплачен до 2027 года, и в ближайшее время докину ещё на пяток лет вперёд.


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


И в третьих — это автоматизация. На его обновление и поддержку врядли потребуется много ресурсов, так как всякие там renovate да dependabot (в связке CI) почти всё делают за меня.


Кстати, о планах — есть желание сделать морду более информативной, и выводить прямо там свою борду (если демон запущен) с описанием запущенных контейнеров, статистикой, конфигами и прочим. Что-то вроде dockstation, только на минималках.


Монетизировать его или ограничивать функционал для не-pro пользователей смысла не вижу и не планирую от слова совсем (docker hub — привет).

Комментарии (19)


  1. Luchnik22
    05.02.2023 15:40
    +2

    А SSL от cloudflare нельзя использовать?

    И ещё, не пробовали ли просто прописать домены в /etc/hosts? И тем же certbot'ом выпустить локальные сертификаты (по моему они локально один раз и на всегда ставятся)? (Я по работе как раз этим сценарием пользуюсь)


    1. paramtamtam Автор
      05.02.2023 16:06
      +1

      А SSL от cloudflare нельзя использовать?

      В теории, наверное, можно, но не делал так никогда. Если у вас есть опыт и сценарий автоматизации этого - поделитесь?

      В hosts прописывать желание пропало после второго раза. Ровно как и держать локальный резольвер типа dnsmasq на зону. Ты же не один работаешь, а всех в командах учить этому, да на зоопарке систем - совсем печаль-тоска. Да, забыл уточнить в статье - ttl у dns записей, что указывают на локалхост - одни сутки. Так что если моргнет сеть на пару часов, и ttl не игнорится локальным кэшем - проблем не должно быть.

      Для совсем локальных историй есть mkcert (ссылка на него есть в статье), но это опять-таки телодвижения :)


    1. Iv38
      06.02.2023 11:57

      Чтобы получить сертификаты Let's Encrypt для локальных сервисов, надо как-то сделать так, чтобы certbot мог подтвердить, что вы владелец домена. Обычно для этого нужно открыть сервер, где запущен certbot в интернет — это вызывает сложности при локальной разработке. Ну и плюс запихивание нужных доменов в hosts тоже боль. Особенно, когда ты работаешь не один, а командой. У нас для этого есть специальная утилита и это очень неудобно. Так что я попробую внедрить решение подобное описанному в статье.

      SSL от клауда использовать тоже, кажется, не получится. В этом случае он выдаёт сертификат своему реверс-прокси-серверу, который никак не может проксировать трафик на локалхост.


  1. Antra
    05.02.2023 22:14
    +1

    Прошу прощения, но что-то я с одного прочтения не уловил фишку. Чем эта штука (реверс прокси) полезна как таковая - понятно. Но "для остальных" в чем прикол? Поднимать собственные сервисы на чужом (вашем) домене?

    Во-первых — это домен. Он более привлекателен (как мне кажется), и приобретен не у российского регистратора (что важно). 

    Вы говорите, что имя вашего домена indocker.app привлекательно для меня, чтобы я на нем свои сервисы поднимал? Или я вообще все неправильно понял?

    Также вы пишете о работе в команде. Можете пояснить кейс? Ведь если я хочу, чтобы некто мог подключиться к моему сервису, он должен по доменному имени на мою машину ломануться.

    Есть много образов (к примеру, nginxproxy/acme-companion для перевыпуска сертификатов, jwilder/nginx-proxy для перегенерации nginx reverse proxy конфигов при запуске нового контейнера). Ваш образ дает нечто похожее, но на базе Traefik, с более удобной интеграцией с Docker (обработка появления новых контейнеров)?

    Просто если он всего лишь избавляет меня от регистрации собственного домена ценой подвязки всех моих сервисов на ваш - это странно, особенно при групповой работе. Условный indocker.in на 10 лет стоит меньше $50. А "временные" домены так и вообще меньше $2 в год есть.


    1. paramtamtam Автор
      05.02.2023 22:55
      +1

      Речь только о локальном окружении, повторюсь. Вот есть у вас, к примеру, сервис (и не редко не один), что состоит из фронтенда и нескольких бэкэндов, и все они разрабатываются с использованием докера. Чтоб с ними взаимодействовать локально вам, скорее всего надо будет пробросить их порты в хост (обращаясь к ним 127.0 0.1:8080, 127.0.0.1:8081 и так далее). И вот вопрос - что для вас удобнее - держать в голове что и на каком порту крутится, или использовать "человеческие" доменные имена вида api.indocker.app, web.indocker.app и тому подобные? Да чтоб еще и tls сертификат был из коробки для них, валидный всюду? Вот статья как раз про это :) Ничего никуда не привязывается, так как работает исключительно на вашей машине, и работает везде, где есть возможность запустить демон докера; под окнами, линуксом и маком, под amd и arm. Так стало понятнее? Извиняюсь, что не удалось это сразу отразить в статье. Видимо, "глаз уже замылился" несколько


      1. Antra
        06.02.2023 08:51

        Вы верно говорите, описиваемые вами "плюшки" хороши. Я просто не могу полноценный кейс для себя представить. Допустим, я лезу на порты 8080 и 8081 из вашего примера. Они явно не SSL, т.е. преимущество с сертифкатом из коробки здесь не скажется. Оно есть, но не проявится.

        Если я правильно понимаю, преимущества в полной мере проявятся, если если я обращаюсь к другому контейнеру (условный бэкенд) через REST API. Причем делаю это не через API gateway, а как бы "напрямую", от контейнера к контейнеру. При этом, хоть и предполагается взаимодействие исключительно внутри одной машины (ноды), трафик между контейнерами зачем-то шифруется. И вот тут мы подменяем оригинальное "контейнер-контейнер" взаимодествие, заставляя их работать через реверс прокси, чтобы расшифровать на заранее подготовленном сертификате и от реверс прокси к финальному контейнеру таки пустить трафик расшифрованным.

        С портами проблему тоже не понимаю. Я же не делаю expose на IP самой машины с докером, зачем разные порты? В моих мелких pet-поделках я просто создавал сетку (docker network create). Т.е. поднимается несколько контейнеров с именами app, back и т.п., каждому автоматически назначается свой IP из этой сетки, а порты у всех могут быть одинаковые (хоть 80). И обращаться друг к другу они могут по имени контейнера благодаря "встроенному DNS". Если у меня запущены контейнеры с именами MariaDB1 и MariaDB2, то в phpadmin я так и укажу MariaDB1:3306 и MariaDB2:3306 соответственно. Снаружи доступ к phpadmin и прочим "web" интерфейсам через единственный контейнер с reverse proxy, которому и нужен expose (а-ля Ingress в кубере).

        Возможно я действительно испорчен Kubernetes, где для шифрования между подами на бэкенде использовал бы скорее envoy proxy, mesh и т.п., нежели шифровал бы самостоятельно внутри пода.

        Я не столько к вашему решению пристаю, оно красивое, сколько к архитектуре (что за приложение с несколькими контейнерами, взаимодействующими друг с другом по https внутри одной машины без планов выставиться в Интернет, где по-любому понадобится свой домен). Фактически мне просто интересно, насколько распространен ваш кейс. Вдруг я такой тип архитектуры упустил, а где-то он мне был бы полезен, эффективен.


        1. SabMakc
          06.02.2023 09:35
          +2

          Пост об организации взаимодействия разработчик-контейнер, а не для контейнер-контейнер (для контейнер-контейнер и не факт, что заведется, потому как на localhost резолвятся все поддомены).

          Когда множество сервисов крутится на локальной машине - действительно нужно помнить, какой порт какому сервису был назначен. А зачастую порты переиспользуются для разных проектов. И если потом возникает необходимость одновременного запуска 2х контейнеров с одинаковыми портами - то надо править порты, править тестовые скрипты и еще что до кучи.

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

          Кроме того, использование traefik приближает использование сервисов к проду (если traefik используется на проде), что тоже может быть очень хорошим фактором.

          Да, подобную схему можно организовать и локально, на кастомном домене. Но браузеры требуют HTTPS для всего, а самоподписанные сертификаты добавят своей головной боли.

          Так что данное решение для организации локалхоста разработчика вполне неплохо выглядит.


          1. paramtamtam Автор
            06.02.2023 09:38
            +1

            Ответили ровно так, как бы сам и написал, благодарю :3


          1. Antra
            06.02.2023 10:40

            OK, спасибо!


  1. DimaFromMai
    05.02.2023 22:24

    Честно говоря совсем не в теме, но прочитал с удовольствием, спасибо автору, хорошая статья.


    1. paramtamtam Автор
      05.02.2023 22:58
      +1

      Спасибо за теплый отзыв!


  1. lllamnyp
    06.02.2023 05:46
    +3

    Автор сделал полезную тулзу и выложил в открытый доступ без регистрации и СМС? Боже, куда мир катится...

    Спасибо!


    1. paramtamtam Автор
      06.02.2023 09:43
      +3

      Простите меня, но удержаться было слишком сложно


  1. SabMakc
    06.02.2023 10:35

    Спасибо, отличная идея по организации локалхоста разработчика!

    Предложение для расширения функционала:
    Резолвить *.docker.indocker.app на 172.17.0.1. Или в принципе, вместо 127.0.0.1 все резолвить на 172.17.0.1...
    Это позволит использовать домен и для взаимодействия между контейнерами, что может быть полезной фишкой для отладки за счет логгирования/трейсинга запросов.
    Возможно, придется какой-нибудь доп.сервис поднимать...

    Будет замечательное средство отладки окружения )

    Правда остается открытым вопрос безопасности (MitM?) и универсальности подобного решения (docker может и на другой подсети жить).

    P.S. MitM не считаю такой уж угрозой. Просто потому, что домен предполагает узкоспециализированное использование. А если все-таки такая угроза есть - то ничего не помешает подменить DNS записи и вместо локалхоста подсунуть все, что угодно.


    1. paramtamtam Автор
      06.02.2023 10:47
      +1

      Интересное предложение. Давайте так - я могу для вас (теста для) зону завести, скажем, вида *.__habr_sabmaks__.indocker.app, которая будет резольвится в 172.17.0.1. Вы сможете вдоволь наиграться, а после этого поделиться обратной связью. Если подводных комней не встретите (или элегантно их обойдёте) - то конечно впилим это как перманентную фичу


      1. SabMakc
        06.02.2023 11:28
        +1

        Тогда давайте уж *.__docker__.indocker.app и публично тестовой фичей объявить...
        Или не объявлять ) Но в любом случае, домен лучше более обезличенным делать )

        Правда я не знаю, как скоро у меня дойдут руки до "поиграться", тут и с traefik полноценно "поиграться" хочется и с доп сервисами "играться" придется - как понимаю, трейсинг и логи доступа не имеют полноценного GUI в рамках traefik...

        На самом деле есть и текущие потребности для подобной фичи - иногда взаимодействие контейнеров требует полноценных сертификатов (обычно есть обходные пути, но их надо искать и дополнительно тестировать)...

        P.S. Как понимаю, с HTTPS-сертификатом на несколько wildcard-доменов проблем не будет - судя по mkcert.sh, эта возможность уже используется...


        1. paramtamtam Автор
          06.02.2023 11:51
          +2

          Домен-то такой мы завести можем, а вот certbot начинает ругаться:

          An unexpected error occurred:
          Error creating new order :: Cannot issue for "*.__docker__.indocker.app": Domain name contains an invalid character
          

          Так что пользуемся *.x-docker.indocker.app:

          $ dig +noall +answer -t A bar.x-docker.indocker.app
          bar.x-docker.indocker.app. 300	IN	A	172.17.0.1
          
          $ dig +noall +answer -t A foo.bar.x-docker.indocker.app
          foo.bar.x-docker.indocker.app. 300 IN	A	172.17.0.1
          

          Релизнуто в v1.1.1. Играйтесь на здоровье, как будет чем поделиться - сюда или в личку черканите, пожалуйста :3


          1. SabMakc
            06.02.2023 12:00

            Обязательно черкану, если/когда будет чем поделиться )
            В принципе, для полноценной фичи, не хватает простого упоминания о *.x-docker.indocker.app в ReadMe )
            Можно и домен как более-менее постоянный для этой фичи рассматривать )


            1. paramtamtam Автор
              06.02.2023 12:10

              Сперва - полноценный тест, потом - в релиз :) Сможете и PR бабахнуть с юзкейсами, если не сложно. Хорошего дня вам!