В результате будет 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, можно продолжать работать на локальном компьютере. Если нет, переносим проекты на сервер.
- Переносим наши проекты на сервер (
traefikproj1и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.jsoncaserver можно раскомментировать для того, чтобы проверить правильность настройки, но не получать реальный сертификат, так как кол-во сертификатов ограничено
Чтобы увидеть результат работы 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-removermiddleware.
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 контейнера