Skip to content

Instantly share code, notes, and snippets.

@dancheskus
Last active April 15, 2024 10:47
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 21 You must be signed in to fork a gist
  • Save dancheskus/365e9bc49a73908302af19882a86ce52 to your computer and use it in GitHub Desktop.
Save dancheskus/365e9bc49a73908302af19882a86ce52 to your computer and use it in GitHub Desktop.
Traefik, как обратный прокси в Docker (пример с 2 react проектами)

Traefik, как обратный прокси в Docker (пример с 2 react проектами)

В результате будет 2 react проекта на 1 сервере доступных по разным ссылкам

Цели

  • Запустить traefik в одном контейнере
  • Запустить другие проекты в других контейнерах
  • Соединить все контейнеры в одну docker cеть
  • Настроить контейнеры с проектами так, что-бы они объясняли traefik'у, какие url ведут на конкретный проект
  • Получить ssl сертификаты для всех проектов используя DNS сhallenge (wildcard)
  • Менять www.example.com на example.com и http://example.com на https://example.com

Перед началом

Перед тем как начать, должно быть следующее

  • Ubuntu сервер с открытыми портами 80 и 443.
  • Установленный docker и docker-compose на локальном компьютере и сервере.
  • Зарегистрированные доменные имена. В этом руководстве я буду использовать proj1.com и proj2.com для двух проектов.
    • Запись A для proj1.com и proj2.com, указывает на публичный IP адрес нашего сервера.
    • Запись A для www.proj1.com и www.proj2.com, указывает на публичный IP адрес нашего сервера.
  • Возможность получить wildcard сертификат для этих доменов

В этом руководстве будет использоваться

  • nodejs
  • create-react-app
  • yarn
  • docker
  • docker-compose

В любой момент может понадобится

  • docker ps - список запущенных контейнеров
  • docker network ls - список всех активных и неактивных сетей созданных docker'ом
  • docker system prune - очистка всех остановленных контейнеров и неиспользуемых сетей
  • docker-compose down - выполняется из корня проекта. Останавливает запущенный контейнер.

Настройка на локальном компьютере

Этап 1. Создаем 2 react проекта

В результате будет 2 простых проекта

В любом удобном месте на локальном компьютере создаем react проект proj1 и proj2

yarn create react-app proj1 yarn create react-app proj2

По очереди запускаем каждый из проектов yarn start и вносим маленькие изменения, чтобы видеть отличия проектов.

Например в src -> App.js каждого проекта вместо Edit <code>src/App.js</code> and save to reload. напишем proj1 и proj2 соответственно.

Этап 2. Упаковываем каждый проект в контейнер

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

Пишем инструкции для запуска контейнера

В корне проекта создаем .env

PROJ_NAME=proj1
DOMAIN=proj1.com

.env - файл в котором задекларированы глобальные переменные рабочей среды (environment). В данном случае эти переменные важны для запуска контейнера. PROJ_NAME - уникальное название проекта, которое будет использоваться в нескольких местах в docker-compose. Название переменной может быть любым. DOMAIN - доменное имя по которому будет доступен проект


Далее в корне проекта создаем dockerfile

dockerfile - это инструкция по созданию контейнера

FROM mhart/alpine-node:latest
  # Качаем node контейнер с docker hub
RUN yarn global add serve
  # Устанавливаем serve для сервирования нашего проекта
WORKDIR /app
  # Указываем главную папку в контейнере, в которой будет наш проект
COPY package.json yarn.lock ./
  # Копируем файлы package.json и yarn.lock в папку, указанную выше (не забываем ./)
RUN yarn
  # Устанавливаем зависимости
COPY . .
  # Копируем остальные файлы проекта в главную папку
RUN yarn build
  # Компилируем проект
CMD serve -s /app/build
  # Серверуем проект из новой папки build (по умолчанию порт 5000)

Далее в корне проекта создаем docker-compose.yml

docker-compose - это инструкция по запуску контейнер(а/ов) Для знающих структуру docker-compose, я не указываю version: '3.x', так как это не является обязательным с версии 1.27.0

services:
# Объявляем список сервисов
  proj1:
  # Указываем название сервиса. Так как это ключ, взять его из .env не получится.
    container_name:  ${PROJ_NAME}
    # Указываем название контейнера (не обязательно). PROJ_NAME берется из .env
    build: .
    # Запускаем dockerfile из корня проекта
    restart: always
    # Автоматически перезагружать контейнер, если он случайно выключился
    environment:
      NODE_ENV: production
      # ENV переменная production сообщит yarn, что не нужно качать зависимости для разработки, если такие имеются. 
    ports:
    # Слева порт, который будет доступен на локальной машине. Справа - тот, к которому нужен доступ в контейнере.
      - 3000:5000
      # 5000 - порт который по умолчанию открывает serve в dockerfile

В корне проекта создаем .dockerignore

.dockerignore - это список исключаемых из копирования COPY . . файлов и папок в dockerfile

.git
node_modules
build

Запускаем и проверяем контейнер

Из корня проекта выполняем docker-compose up

При первом запуске будут выполняться все инструкции dockerfile, что займет какое-то время. Дальнейшие запуски, при отсутствии изменений в проекте, будут проходить быстрее.

Когда видим строку вида proj1 | INFO: Accepting connections at http://localhost:5000, можем проверить в браузере localhost:3000. Результатом должен быть react проект с надписью proj1.

В командной строке нажимаем ctrl+c, чтобы выйти из контейнера и проверяем проект proj2

Этап 3. Создаем 2 фейковых url для теста

В результате будет 2 адреса, каждый из которых будет вести на localhost. Нужно для дальнейшей настройки traefik.

В hosts файле на компьютере добавляем запись

127.0.0.1       proj1.com
127.0.0.1       proj2.com

Этап 4. Создаем traefik контейнер

В результате будет traefik контейнер, который будет работать на 80 порту

Создаем docker-compose для traefik

В удобном месте создаем папку traefik.

Затем в ней создаем файл docker-compose.yml

services:
  traefik:
  # Указываем название сервиса
    container_name: traefik
    # Указываем название контейнера (не обязательно)
    image: traefik:latest
    # Вместо dockerfile берем готовый traefik (:latest - последняя версия) с docker hub
    restart: always
    ports:
      - "80:80"
      # Порт 80 будет доступен как в контейнере, так и снаружи.
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      # Позволяет следить traefik'у отслеживать все изменения в docker.
      # Нужно для отслеживания подключения/отключения других контейнеров.
      - ./:/etc/traefik
      # Переносит файл traefik.yml с основными настройками в контейнер.

volumes - позволяет использовать файлы из локального компьютера не копируя их в контейнер. В данном случае мы берем файл traefik.yml, который мы создадим дальше, в этой-же папке и "говорим" контейнеру, что это файл, который в контейнере будет находится здесь: /etc/traefik

Создаем traefik.yml

traefik.yml - это статическая конфигурация traefik. Позже мы добавим и динамическую конфигурацию.

В папке traefik создаем файл traefik.yml

# log:
#   level: DEBUG
# Раскомментировать только в случае необходимости выведения в консоль всей информации о работе traefik

providers:
  docker: true
  # Говорит traefik'у, что мы работаем с docker

entrypoints:
  web:
  # Название точки входа, к которой будут подключатся другие контейнеры.
  # может быть любым. Главное, чтобы совпадал с названием в других местах.
    address: :80
    # traefik будет доступен на 80 порту.

Запускаем traefik контейнер

В папке traefik выполняем docker-compose up. В консоле должно появится сообщение Configuration loaded from file: /etc/traefik/traefik.yml, что означает, что файл traefik.yml успешно импортирован в контейнер, и настройки traefik считываются с него. В браузере при переходе на localhost, мы должны видеть 404 page not found, что означает, что traefik работает, но страницы с таким адресом не найдено.

Останавливаем контейнер и переходим далее

Этап 5. Связываем контейнеры между собой

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

Создаем общую сеть, к которой будут подключатся все контейнеры

В терминале пишем: docker network create traefik

Где traefik, название сети (может быть любым. Главное, чтобы совпадало с настройками далее)

Меняем docker-compose файл в traefik проекте

В папке traefik в файле docker-compose под блоком services пишем новый блок networks

networks:
# Блок для объявления внутренних docker сетей, к которым нужно будет подключить контейнер.
  default:
    external: 
      name: traefik
      # Название созданной выше сети
Результат
services:
  traefik:
    container_name: traefik
    image: traefik:latest
    restart: always
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./:/etc/traefik

networks:
  default:
    external: 
      name: traefik

Меняем docker-compose файл в react проекте

Действия описанные далее для proj1 нужно выполнять для каждого проекта.

В папке proj1 в файле docker-compose меняем

ports:
  - 3000:5000

на

ports: [5000]

Контейнер по прежнему слушает на порту 5000, но уже не открывает его для локального компьютера. Этот порт теперь доступен только контейнерам в той-же сети.

Под блоком services пишем

networks:
  default:
    external: 
      name: traefik

В блоке services в подблок proj1 добавляем

labels:
  - "traefik.http.routers.${PROJ_NAME}.rule=Host(`${DOMAIN}`)"
  - "traefik.http.routers.${PROJ_NAME}.entrypoints=web"

PROJ_NAME и DOMAIN берутся из .env

Краткое объяснение:

Мы хотим передать некоторые настройки из этого контейнера в traefik. Так как traefik умеет читать yaml формат, то в идеале было бы хорошо передать их именно в этом формате. Но у нас нет такой возможности. Обходным путем является записи в labels. Важно помнить, что мы оставляем структуру yaml. Вот пример того, как это бы выглядело в yaml формате:

traefik:
  http:
    routers:
      proj1:
      # Это наше уникальное название, но оно должно быть одинаковым для для настроек этого контейнера.
      # Именно по этому в "labels", proj1 используется в нескольких местах. Берем из ".env".
      # Для proj2 это название может быть proj2
        rule: Host(`proj1.com`)
        # proj1.com это наш домен. Берем из ".env".
        entrypoints: web
        # это то название, которое записано в traefik.yml

Так как ранее, в настройках traefik мы указали

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

то при подключении нового контейнера, traefik сможет считывать его labels, и сможет интерпретировать их как yaml с настройками.

Результат
services:
  proj1:
    container_name: ${PROJ_NAME}
    build: .
    restart: always
    environment:
      NODE_ENV: production
    ports: [5000]
    labels:
      - "traefik.http.routers.${PROJ_NAME}.rule=Host(`${DOMAIN}`)"
      - "traefik.http.routers.${PROJ_NAME}.entrypoints=web"

networks:
  default:
    external: 
      name: traefik

В Host() нужно записывать домен именно в косых кавычках ``

Запускаем все контейнеры traefik, proj1 и proj2. В результате, вводя в браузере proj1.com и proj2.com мы попадаем на наши 2 проекта.

Этап 6. Добавляем поддержку ssl

В результате проекты будут доступны по ссылкам https://proj...

Если сейчас мы попытаемся посетить https://proj1.com, то проект будет не доступен. Для того, чтобы это изменить, необходимо произвести некоторые изменения в настройках как traefik так и проектах.

В папке traefik в файле docker-compose в блок ports добавляем

-  "443:443"

тем самым открывая доступ по https снаружи.

В файле traefik.yml в блок entrypoints добавляем

websecure:
# Также как и название "web" может быть любым. Главное, чтобы именно оно использовалось далее.
  address: :443

Под блоком entrypoints добавляем

certificatesresolvers:
  myresolver:
  # Название может быть любым. К этому блоку будут обращатся контейнеры для получения сертификатов
    acme:
    # Здесь будут находится настройки для получения сертификатов.
      storage: /etc/traefik/acme.json
      # Включаем сохранение информации о полученых сертификатах в папку "traefik",
      # чтобы не генерировать их каждый раз при запуске контейнера.
      # Если сертификат ранее был создан и является актуальным,
      # то traefik возьмет его из файла "acme.json"
Результат
# log:
#   level: DEBUG

providers:
  docker: true

entrypoints:
  web:
    address: :80
  websecure:
    address: :443

certificatesresolvers:
  myresolver:
    acme:
      storage: /etc/traefik/acme.json

В папке proj1 в файле docker-compose в блоке labels заменяем entrypoints

- "traefik.http.routers.${PROJ_NAME}.entrypoints=web"

на выше созданный websecure

- "traefik.http.routers.${PROJ_NAME}.entrypoints=websecure"

также добавляем

- "traefik.http.routers.${PROJ_NAME}.tls.certresolver=myresolver"
# Где "myresolver", это название резолвера созданного выше

Запускаем все контейнеры traefik, proj1 и proj2. В результате, вводя в браузере https://proj1.com и https://proj2.com мы попадаем на наши 2 проекта.

Так как сертификаты не прошли необходимых проверок let's encrypt, необходимо в браузере дать разрешение на использование сайта с этими сертификатами. Можно также в терминале ввести curl -k https://proj1.com. -k поможет вывести html без проверки сертификата.

Этап 7. Переносим контейнеры на сервер, если нужно

Возможно вы хотите работать не только с доменами ведущими на сервер, но и поддоменами, которые ведут на 127.0.0.1 (localhost). Например, набирая proj1.com вы попадаете на сервер, а набирая dev.proj1.com, попадаете на localhost для локальной разработки. В таком случае, необходимо, чтобы:

  • Запись A для dev.proj1.com и dev.proj2.com, указывала на 127.0.0.1.
  • Запись A для www.dev.proj1.com и www.dev.proj2.com, указывала на 127.0.0.1.

Соответственно, на локальном компьютере в проектах в .env указываются домены ведущие на localhost, а на сервере указываются домены ведущие на сервер.

Если есть домены ведущие на localhost, можно продолжать работать на локальном компьютере. Если нет, переносим проекты на сервер.

  1. Переносим наши проекты на сервер (traefik proj1 и proj2)
  2. Заменяем названия проектов
    • Название подблока services в docker-compose.yml
    • PROJ_NAME в .env
  3. Заменяем домены proj1.com в проектах на необходимые
    • DOMAIN в .env

Для удобства я продолжу использовать домены proj1 и proj2.

Этап 8. Настройка резолвера (ACME-клиента)

В данный момент мы у нас еще нет настоящих сертификатов. Для их получения необходимо настроить acme клиент.

На странице https://doc.traefik.io/traefik/https/acme/ ищем конфигурацию для своего DNS провайдера. Далее я привожу пример того, как это сделать с cloudflare.

На сайте указано, что для подключения к cloudflare понадобится CF_DNS_API_TOKEN. Получив токен нужно сделать его доступным в environment при запуске docker.

В папке traefik создаем файл .env и пишем туда токен

CF_DNS_API_TOKEN=токен

В файле docker-compose.yml в подблоке traefik добавляем

environment:
  CF_DNS_API_TOKEN:  ${CF_DNS_API_TOKEN}

Токен передастся из .env в traefik


В файле traefik.yml меняем блок acme

certificatesresolvers:
  myresolver:
    acme:
      # caserver: "https://acme-staging-v02.api.letsencrypt.org/directory"
      dnsChallenge:
        provider: cloudflare
      email: ваш_email
      storage: /etc/traefik/acme.json

caserver можно раскомментировать для того, чтобы проверить правильность настройки, но не получать реальный сертификат, так как кол-во сертификатов ограничено Чтобы увидеть результат работы acme клиента нужно также раскомментировать log: level: DEBUG перед запуском контейнера.

Если вы, при запуске контейнера, где-то в консоли видите эти строки, значит все в порядке. Можно закомментировать caserver и повторить процедуру для получения сертификатов.

[INFO] [доменные_имена] acme: Validations succeeded; requesting certificates"
[INFO] [доменные_имена] Server responded with a certificate."

Этап 9. Убираем www и заменяем http:// на https://

В папке proj1 в файле docker-compose добавляем labels

- "traefik.http.routers.${PROJ_NAME}.tls.domains[0].main=${DOMAIN}"
  # Указывает на то, что основной домен это ${DOMAIN}
- "traefik.http.routers.${PROJ_NAME}.tls.domains[0].sans=www.${DOMAIN}"
  # Указывает на то, что дополнительный домен это www.${DOMAIN}

Теперь проект будет доступен по ссылкам как с "www" так и без.


В папке traefik в файле traefik.yml меняем entrypoints

entrypoints:
  web:
    address: :80
    http:
      redirections:
        entrypoint:
          to: websecure
          scheme: https
  websecure:
    address: :443

Теперь все http:// запросы будут превращаться в https://


В папке traefik создаем файл dynamic_conf.yml

http:
  middlewares:
    www-remover:
      redirectregex:
        regex: ^https://www\.(.*)
        replacement: https://$1

  routers:
    www-router:
      rule: HostRegexp(`{host:www\..+}`)
      tls: true
      service: noop@internal
      middlewares: www-remover

tls:
  options:
    default:
      sniStrict: true

В middlewares описано то, что делать с ссылками, которые поступают из роутеров. Если у ссылки есть "www.", вызывается www-remover middleware.

sniStrict: true использовать, если необходимо заблокировать раздачу страниц с несуществующих поддоменов. Например correct_page.proj1.com будет доступен, а wrong_page.proj1.com - нет.


В traefik.yml добавляем дополнительный провайдер, для применения динамической конфигурации из файла

providers:
  docker: true
  file:
    filename: /etc/traefik/dynamic_conf.yml
    # указываем на выше созданный файл

Теперь traefik обрабатывает не только labels из docker, но и подгруженный файл.


Готово

Дополнительно

Отдельный docker контейнер доступный по url/example_container

Если proj1.com уже получил сертификат и необходимо добавить отдельный сервис на proj1.com/example_container, то структура docker-compose будет следующая

название_сервиса:
  container_name: название_контейнера
  прочие_настройки: ...
  labels:
    - "traefik.http.routers.${название_для_traefik}.rule=Host(`${DOMAIN_ранее_получивший_сертификат}`) && PathPrefix(`/example_container`)"
    - "traefik.http.routers.${название_для_traefik}.tls.certresolver=myresolver"

Ссылки

Пример настройки traefik

Пример настройки react контейнера

@dedops
Copy link

dedops commented Jul 26, 2022

Добрый день! У меня аналогичная схема, но есть нюанс. Если идти напрямую на порт 5000(react), он корректно работает по http, общается с бэком также по http.
Мне нужно сделать,чтобы пользователь в браузере работал по https. Это примерно то, что описывается в статье. Ставим перед реактом Traefik, он принимает запрос по https, а дальше обращается к react. Но судя по всему, он не идет по http, а продолжает идти по https, после чего не отвечает бэк, т.к. он не умеет в https.

Хотелось бы следующую схему Client(https) - [Traefik (https) <-> Traefik(http)] - React(http) -------
Возможно ли это? И как реализовать, если да?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment