В результате будет 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
- выполняется из корня проекта. Останавливает запущенный контейнер.
В результате будет 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
соответственно.
В результате каждый проект будет в отдельном контейнере. Все действия далее описываю только для одного проекта. Их нужно сделать для двух проектов.
В корне проекта создаем .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
В результате будет 2 адреса, каждый из которых будет вести на
localhost
. Нужно для дальнейшей настройкиtraefik
.
В hosts
файле на компьютере добавляем запись
127.0.0.1 proj1.com
127.0.0.1 proj2.com
В результате будет
traefik
контейнер, который будет работать на80
порту
В удобном месте создаем папку 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. Позже мы добавим и динамическую конфигурацию.
В папке traefik
создаем файл traefik.yml
# log:
# level: DEBUG
# Раскомментировать только в случае необходимости выведения в консоль всей информации о работе traefik
providers:
docker: true
# Говорит traefik'у, что мы работаем с docker
entrypoints:
web:
# Название точки входа, к которой будут подключатся другие контейнеры.
# может быть любым. Главное, чтобы совпадал с названием в других местах.
address: :80
# traefik будет доступен на 80 порту.
В папке traefik
выполняем docker-compose up
. В консоле должно появится сообщение Configuration loaded from file: /etc/traefik/traefik.yml
, что означает, что файл traefik.yml
успешно импортирован в контейнер, и настройки traefik
считываются с него.
В браузере при переходе на localhost
, мы должны видеть 404 page not found
, что означает, что traefik
работает, но страницы с таким адресом не найдено.
Останавливаем контейнер и переходим далее
В результате
traefik
контейнер сможет перенаправлять запросы в другие контейнеры.
В терминале пишем: docker network create traefik
Где
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
Действия описанные далее для
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 проекта.
В результате проекты будут доступны по ссылкам 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 без проверки сертификата.
Возможно вы хотите работать не только с доменами ведущими на сервер, но и поддоменами, которые ведут на 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
, можно продолжать работать на локальном компьютере. Если нет, переносим проекты на сервер.
- Переносим наши проекты на сервер (
traefik
proj1
иproj2
) - Заменяем названия проектов
- Название подблока
services
вdocker-compose.yml
PROJ_NAME
в.env
- Название подблока
- Заменяем домены
proj1.com
в проектах на необходимыеDOMAIN
в.env
Для удобства я продолжу использовать домены
proj1
иproj2
.
В данный момент мы у нас еще нет настоящих сертификатов. Для их получения необходимо настроить 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."
В папке 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, но и подгруженный файл.
Готово
Если 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 контейнера
Добрый день! У меня аналогичная схема, но есть нюанс. Если идти напрямую на порт 5000(react), он корректно работает по http, общается с бэком также по http.
Мне нужно сделать,чтобы пользователь в браузере работал по https. Это примерно то, что описывается в статье. Ставим перед реактом Traefik, он принимает запрос по https, а дальше обращается к react. Но судя по всему, он не идет по http, а продолжает идти по https, после чего не отвечает бэк, т.к. он не умеет в https.
Хотелось бы следующую схему Client(https) - [Traefik (https) <-> Traefik(http)] - React(http) -------
Возможно ли это? И как реализовать, если да?