Для запуска веб-приложений нужен веб-сервер, программа, которая принимает входящие HTTP-запросы и передает их на обработку приложению. Приложение делает свою работу и отдает результат веб-серверу, который формирует HTTP-ответ для клиента.
Часто такой веб-сервер либо встроен в сам язык как модуль, либо написан на этом же языке, для запуска кода внутри себя. Обычно, задачу интеграции берут на себя фреймворки. В нашем приложении devops-example-app за это отвечает фреймворк fastify. Внутри себя он использует встроенный в Node.js веб-сервер:
import http from 'http';
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('okay');
}).listen(8080);
Несмотря на простоту такой схемы, ее в большинстве проектов недостаточно. Помимо встроенного веб-сервера, нужен внешний, выполняющий роль реверс-прокси (reverse-proxy), например Nginx или Caddy.
Прокси-сервер ставится между клиентами и внутренней инфраструктурой. Он принимает запросы от клиентов и переадресует их на внутренний веб-сервер или веб-сервера, если их много. Именно поэтому он называется реверс (обратный), снаружи внутрь. Зачем он нужен?
- Проекты редко состоят только из кода, который мы запускаем на встроенном веб-сервере. Помимо кода, почти всегда есть файлы, css, js, картинки и другие файлы. Для их "раздачи" тоже нужен веб-сервер, а еще нужно уметь их сжимать и кешировать
- Далеко не все внутренние веб-сервера умеют эффективно обрабатывать "медленных клиентов". Это клиенты с медленным соединением, которые очень долго отправляют и принимают данные. Они занимают ресурсы веб-сервера и приложения. Если придет достаточное количество медленных клиентов, то веб-сервер и приложение быстро перестанут отвечать. Пользователи начнут видеть ошибки
- Встроенные веб-сервера достаточно ограниченны в своих возможностях, например в том как они обрабатывают ошибки, как поддерживают HTTPS и так далее
Эти причины настолько серьезны, что без реверс-прокси в продакшен не ходят. Только какой выбрать? Nginx самый популярный сервер в мире, но Caddy, гораздо проще в настройке и отладке. У него встроенная поддержка Let's Encrypt, Caddy автоматически обновляет сертификаты для работы HTTPS. Поэтому в этом курсе мы воспользуемся Caddy. Изучив его, вы без труда сможете перейти на Nginx, так как концептуально все подобные веб-сервера работают одинаково.
Caddy
Для начала Caddy нужно подключить к локальной разработке. Так мы сможем тестировать его конфигурацию еще до выкладки в продакшен.
Локальное окружение
Когда наш проект состоял из одного приложения, его было достаточно разрабатывать напрямую без Docker Compose. Как только добавляется сервис, в нашем случае внешний веб-сервер Caddy, то без Docker Compose становится сложно. Появляется задача управлять набором сервисов как единым целым, стартовать их, останавливать и перезапускать.
Добавим Docker Compose с такой конфигурацией:
# file: docker-compose.yml
version: "3"
services:
app: # Имя сервиса
build:
# Контекст для сборки образа,
# в данном случае, текущая директория
context: .
# Имя Docker-файла из которого будет собран образ
dockerfile: Dockerfile
volumes:
- .:/app
ports:
- 3000:3000
caddy: # Веб-сервер
build: # Находится внутри services/caddy
context: .
dockerfile: services/caddy/Dockerfile
volumes:
- .:/app
- ./services/caddy/Caddyfile:/etc/caddy/Caddyfile
ports:
- 80:80
- 443:443
Почему именно так? Мы придерживаемся структуры, в которой основное приложение располагается в корне проекта, а дополнительные сервисы лежат в директории services. У каждого сервиса своя директория с Dockerfile внутри.
# services/caddy/Dockerfile
# У Caddy есть официальный образ, но его недостаточно
# В продакшене конфигурация должна быть частью образа
# Поэтому мы делаем свой образ, в который "зашиваем" конфигурацию
FROM caddy
# Конфигурационный файл Caddy
# Читается из /etc/caddy/Caddyfile
COPY services/caddy/Caddyfile /etc/caddy/
# Копируем приложение для доступа к статическим файлам хранящимся в репозитории
# Обычно это элементы интерфейса
COPY . /app
Там же хранятся дополнительные файлы конкретного сервиса. Например у Caddy это файл Caddyfile, в котором содержится конфигурация:
# Домены, которые будет обслуживать Caddy
# Первый домен для локальной разработки, второй для продакшена (воображаемого)
localhost, devops-example.test {
# Формируем самоподписной сертификат для работы https
tls internal
# Отдаем картинки напрямую из файловой системы минуя приложение
# Картинки в нашем проекте хранятся в /app/public внутри образа с Caddy
# Выше в Dockerfile мы туда их копируем
# /images/app.png => /app/public/images/app.png
handle /images/* {
file_server
root * /app/public
}
# Все остальные запросы передаются в приложение
handle {
# app - имя домена во внутренней сети Docker
# В девелопменте совпадает с именем сервиса в docker-compose.yml
# Для продакшена надо будет создать общую сеть
reverse_proxy app:3000
}
# Включаем логгирование для удобной отладки
log {
format json
}
}
После того как все файлы добавлены, можно запустить проект локально и убедиться что он работает:
docker compose up
docker compose up --abort-on-container-exit
[+] Running 2/0
⠿ Container devops-example-app-caddy-1 Created
⠿ Container devops-example-app-app-1 Created
Attaching to devops-example-app-app-1, devops-example-app-caddy-1
# Урезанный вывод
devops-example-app-caddy-1 | {"level":"info","ts":1646952470.170849,"msg":"serving initial configuration"}
devops-example-app-app-1 | > fastify start server/plugin.js -a 0.0.0.0 -l info -P
devops-example-app-app-1 | 22:48:50 ✨ Server listening at http://0.0.0.0:3000
После этого запрос к localhost пойдет в Caddy, который выполнит автоматическое перенаправление на https. Браузер укажет на недоверенный сертификат, что нормально, так как он самоподписной. Все что остается сделать, чтобы увидеть сайт - подвердить что вы согласны с риском.
Продакшен
Первым делом добавим Caddy в сборку на коммит. Для этого создадим репозиторий для Caddy на Docker Hub и расширим воркфлоу main.yml:
# .github/workflows/main.yml
# Добавляем сразу после сборки app
- name: Build and push caddy
uses: docker/build-push-action@v2
with:
context: .
file: services/caddy/Dockerfile
push: true
# Не забудьте создать репозиторий в Docker Hub
tags: hexletcomponents/devops-example-caddy:latest
Следующим шагом добавим в релиз:
# .github/workflows/release.yml
# Добавляем сразу после сборки app
- run: docker pull hexletcomponents/devops-example-caddy:latest
- run: docker tag hexletcomponents/devops-example-caddy:latest hexletcomponents/devops-example-caddy:${{ github.ref_name }}
- run: docker push hexletcomponents/devops-example-caddy:${{ github.ref_name }}
Когда файлы добавлены, их нужно закоммитить и выполнить релиз. Если воркфлоу отработал нормально, значит образ с правильным тегом успешно собрался. Пора переходить к деплою.
Если просто добавить старт контейнера в плейбук ansible/release.yml, то сайт не откроется. Caddy попытается обратиться к указанному в конфигурации app:3000 и выдаст в лог ошибку о недоступности домена. Почему? Каждый контейнер в Docker запускается в изолированной среде и не знает про другие контейнеры. Для их связи используется network.
tasks:
# Создаем общую сеть
# Контейнеры добавленные в эту сеть, могут обращаться друг к другу по имени контейнера,
# который становится dns именем
- community.docker.docker_network:
name: devops-example
- community.docker.docker_container:
name: app # имя используется как домен
image: "hexletcomponents/devops-example-app:{{ version }}"
restart_policy: always
state: started
networks: # Добавляем контейнер в сеть
- name: devops-example
ports: # Больше не нужно выставлять наружу, потому что доступ идет через Caddy
# - 3000:3000
env:
NODE_ENV: production
- community.docker.docker_container:
name: caddy # имя используется как домен
image: "hexletcomponents/devops-example-caddy:{{ version }}"
restart_policy: always
state: started
networks: # Добавляем контейнер в сеть
- name: devops-example
ports:
- 80:80
- 443:443
Когда все добавлено, выполните деплой последнего релиза:
ansible-playbook release.yml -i inventory.yml --extra-vars="version=v6" -vv
Наш проект выложен на сервер и работает. Технически мы можем обратиться к нему по ip-адресу, но так делать не стоит. ip-адрес всегда меняется при пересоздании сервера, из-за чего постоянно придется запоминать новый. Запоминать ip-адрес, в принципе, не очень удобно. Лучше иметь доменное имя, которое никогда не меняется.
Самый простой способ добавить доменное имя для разработки, внести запись в файл /etc/hosts. Этот файл часть системы DNS, которая выполняет поиск ip-адреса по имени. Поиск всегда начинается с него:
# Укажите тут адрес вашей машины, которую вы создали
65.108.149.193 devops-example.test
Попробуйте открыть в браузере devops-example.test. Если все было сделано правильно, то вы увидите сайт.
Обработка ошибок
Во время деплоя приложение перезапускается. Между остановкой и стартом новой версии проходит какое-то время, когда приложение не работает, а пользователи видят ошибку 502 Bad Gateway. Иногда это время достаточно большое, есть приложения которые стартуют минуты.
Обработка ошибки 502 задача реверс-прокси. Для этого создается html-файл, в котором красиво описывается ситуация "мы обновляемся". Этот файл добавляется в образ к веб-серверу, а в конфигурации прописываются такие строки:
# Caddy
reverse_proxy {
to app:3000
@error status 502 # Только 502 ошибки
handle_response @error {
# Путь по которому хранятся html-файлы для разных ошибок
root * /app/services/caddy/error_pages
# Для всех 502 запросов отдаем файл 502.html
# Имя произвольное
rewrite * /502.html
file_server
}
}
# Nginx
# Имя файла произвольное
error_page 502 /502.html;
location = /502.html {
root /path/to/files/with/html;
internal;
}
Учтите, что эта ошибка возникает не только во время деплоя. Если приложение упало, по какой-либо причине, то мы получим тоже самое.
Самостоятельная работа
-
Разверните свое приложение с помощью Docker Compose по примеру из теории. Сделайте так, чтобы оно работало с Caddy.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.