В средних и больших проектах сайт не ограничивается одним сервисом — к примеру только сайтом, как правило существует база данных, API, сервер который маршрутизирует запросы ко всем этим сервисам. Выкатывать и обновлять все это без какой-либо стандартизации непросто, а масштабировать на множество серверов еще сложнее.
Решить эту проблему нам поможет docker — ставший стандартом де-факто в мире упаковки, доставки и публикации приложений.
Docker позволяет нам обернуть приложение или сервис со всеми зависимостями и настройками в изолированный контейнер, гарантируя консистентность содержимого на любой платформе.
В качестве изоморфного приложения мы будем использовать фреймворк Nuxt.js, который состоит из Vue.js и Node.js, позволяя писать универсальные веб-приложения с отрисовкой на стороне сервера (SSR).
Данный выбор обусловлен личным предпочтением, однако аналогичным образом можно взять любой другой фреймворк, например Next.js.
Собираем и публикуем первый образ.
Прежде всего необходимо настроить порт и хост внутри приложения. Существует несколько способов это сделать, мы воспользуемся настройками в package.json, добавив новую секцию:
"config": {
"nuxt": {
"host": "0.0.0.0",
"port": "3000"
}
}
Для дальнейших действий нам потребуется docker, docker-compose установленные в системе и редактор с открытым проектом.
Создадим Dockerfile который поместим в корень и опишем инструкции для сборки образа.
Нам необходимо собрать образ базируясь на образе Node.js версии 10, в данном случае используется облегченная версия alpine:
FROM node:10-alpine
Затем установим переменную окружения с названием директории:
ENV APP_ROOT /web
Установим в качестве рабочей директории и добавим исходники:
WORKDIR ${APP_ROOT}
ADD . ${APP_ROOT}
Устанавливаем зависимости и собираем приложение:
RUN npm ci
RUN npm run build
И пишем команду запуска приложения внутри образа:
CMD ["npm", "run", "start"]
FROM node:10-alpine
ENV APP_ROOT /web
ENV NODE_ENV production
WORKDIR ${APP_ROOT}
ADD . ${APP_ROOT}
RUN npm ci
RUN npm run build
CMD ["npm", "run", "start"]
После чего открываем в терминале текущую папку и собираем образ:
docker build -t registry.gitlab.com/vik_kod/nuxtjs_docker_example .
Запускаем образ локально для проверки что все работает корректно:
docker run -p 3000:3000 registry.gitlab.com/vik_kod/nuxtjs_docker_example
Перейдя по адресу localhost:3000 мы должны увидеть следующее:
Отлично! Мы успешно запустили production сборку приложения на локальной машине.
Теперь нам необходимо опубликовать образ в docker репозиторий, для того чтобы на целевом сервере использовать готовый собранный образ. Можно использовать как self-hosted репозиторий так и любой другой, например официальный hub.docker.com.
Я воспользуюсь репозиторием в gitlab, вкладка с docker репозиториями там называется registry. Предварительно я уже создал репозиторий для проекта поэтому сейчас выполняю команду:
docker push registry.gitlab.com/vik_kod/nuxtjs_docker_example
После того как образ успешно загрузился можно приступить к конфигурации
у моего она следующая:
- 1 ГБ оперативной памяти
- 4 ядра
- 30 ГБ диск
Также я воспользовался возможностью поставить docker сразу при создании сервера, поэтому если на вашем
После создания сервера заходим на него и логинимся в docker репозитории, в моем случае это gitlab:
docker login registry.gitlab.com
После авторизации мы можем запустить приложения ранее виденной командой:
docker run -p 3000:3000 registry.gitlab.com/vik_kod/nuxtjs_docker_example
Образ скачался и запустился, давайте проверим:
Видим знакомую картину, мы запустили контейнер с приложением, но уже на удаленном сервере.
Остался последний штрих, сейчас при закрытии терминала образ будет остановлен, поэтому добавим атрибут -d для того, чтобы запустить контейнер в фоне.
Останавливаем и перезапускаем:
docker run -d -p 3000:3000 registry.gitlab.com/vik_kod/nuxtjs_docker_example
Теперь можем закрыть терминал и убедиться, что наше приложение успешно функционирует.
Мы добились необходимого — запустили приложение в docker и теперь оно пригодно для развертывания, как самостоятельный образ, так и в рамках более масштабной инфраструктуры.
Добавляем reverse proxy
На текущем этапе мы можем публиковать простые проекты, но что если нам нужно поместить приложение и API на одном домене и в дополнение к этому отдавать статику не через Node.js?
Таким образом появляется необходимость так называемого reverse proxy сервера, на который будут поступать все запросы и перенаправляться в зависимости от запроса к связанным сервисам.
В качестве такого сервера мы будем использовать nginx.
Управлять контейнерами если их больше чем один по отдельности не очень удобно. Поэтому мы воспользуемся docker-compose как способом организации и управления контейнерами.
Создадим новый пустой проект, в корень которого добавим файл docker-compose.yml и папку nginx.
В docker-compose.yml пишем следующее:
version: "3.3"
# Указываем раздел со связанными сервисами
services:
# Первый сервис, nginx
nginx:
image: nginx:latest
# Пробрасываем порты 80 для http и 443 для https
ports:
- "80:80"
- "443:443"
# Опциональный параметр с именем контейнера
container_name: proxy_nginx
volumes:
# Используем свой nginx конфиг, он заменит дефолтный в контейнере
- ./nginx:/etc/nginx/conf.d
# Монтируем папку с логами на хост машину для более удобного доступа
- ./logs:/var/log/nginx/
# Второй сервис Nuxt.js приложение
nuxt:
# Используем ранее собранный образ
image: registry.gitlab.com/vik_kod/nuxtjs_docker_example
container_name: nuxt_app
# Также пробрасываем порт на котором висит приложение
ports:
- "3000:3000"
В папку nginx добавляем конфиг, который рекомендует официальный сайт Nuxt.js, c небольшими изменениями.
map $sent_http_content_type $expires {
"text/html" epoch;
"text/html; charset=utf-8" epoch;
default off;
}
server {
root /var/www;
listen 80; # Порт который слушает nginx
server_name localhost; # домен или ip сервера
gzip on;
gzip_types text/plain application/xml text/css application/javascript;
gzip_min_length 1000;
location / {
expires $expires;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
# Адрес нашего приложения, так как контейнеры связаны при помощи
# docker-compose мы можем обращаться к ним по имени контейнера, в данном случае nuxt_app
proxy_pass http://nuxt_app:3000;
}
}
Выполняем команду для запуска:
docker-compose up
Все корректно запустилось, теперь если мы перейдем по адресу который слушает nginx, localhost — то увидим наше приложение, визуально отличий не будет, однако теперь все запросы сначала идут на nginx где и перенаправляются в зависимости от указанных правил.
Сейчас у нас нет дополнительных сервисов или статики, давайте добавим папку static в которую поместим какое-нибудь изображение.
Смонтируем её в контейнер nginx добавив строчку в docker-compose:
...
container_name: proxy_nginx
volumes:
# Монтируем папку со статикой
- ./static:/var/www/static
...
version: "3.3"
# Указываем раздел со связанными сервисами
services:
# Первый сервис, nginx
nginx:
image: nginx:latest
# Пробрасываем порты 80 для http и 443 для https
ports:
- "80:80"
- "443:443"
# Опциональный параметр с именем контейнера
container_name: proxy_nginx
volumes:
# Используем свой nginx конфиг, он заменит дефолтный в контейнере
- ./nginx:/etc/nginx/conf.d
# Монтируем папку с логами на хост машину для более удобного доступа
- ./logs:/var/log/nginx/
# Монтируем папку со статикой
- ./static:/var/www/static
# Второй сервис Nuxt.js приложение
nuxt:
# Используем ранее собранный образ
image: registry.gitlab.com/vik_kod/nuxtjs_docker_example
container_name: nuxt_app
# Так же пробрасываем порт на котором висит приложение
ports:
- "3000:3000"
Затем добавим новый location в nginx.conf:
location /static/ {
try_files $uri /var/www/static;
}
map $sent_http_content_type $expires {
"text/html" epoch;
"text/html; charset=utf-8" epoch;
default off;
}
server {
root /var/www;
listen 80; # Порт который слушает nginx
server_name localhost; # домен или ip сервера
gzip on;
gzip_types text/plain application/xml text/css application/javascript;
gzip_min_length 1000;
location / {
expires $expires;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
# Адрес нашего приложения, так как контейнеры связаны при помощи
# docker-compose мы можем обращаться к ним по имени контейнера, в данном случае nuxt_app
proxy_pass http://nuxt_app:3000;
}
location /static/ {
try_files $uri /var/www/static;
}
}
Перезапускаем docker-compose:
docker-compose up --build
Переходим по адресу localhost/static/demo.jpg
Теперь статика отдается через Nginx, снимая нагрузку с Node.js в основном приложении.
Убедившись что все работает, можно публиковать нашу сборку на сервере. Для этого я создам репозиторий в текущей директории. Предварительно добавив папку logs и static в .gitignore.
После чего заходим на сервер, останавливаем ранее запущенный docker образ и клонируем репозиторий.
Прежде чем приступать к запуску сборки, необходимо переместить папку со статикой на сервер, переходим в терминал на локальной машине и через командную утилиту scp перемещаем папку на сервер:
scp -r /Users/vik_kod/PhpstormProjects/nuxtjs_docker_proxy_example/static root@5.101.48.172:/root/example_app/
Если объем статики большой, лучше сначала сжать папку и отправлять архивом, после чего распаковать на сервере. Иначе загрузка может затянуться надолго.
Возвращаемся в терминал на сервере и перейдя в склонированную папку запускаем команду:
docker-compose up -d
Закрываем терминал и переходим на сайт:
Отлично! С помощью reverse proxy мы отделили статику от приложения.
Дальнейшие шаги
Все что мы с вами сделали выше это достаточно простой вариант, в больших проектах необходимо учитывать большей вещей, ниже краткий список того что можно делать дальше.
- Data only контейнеры для статичных админок, SPA приложений и базы данных
- Дополнительные сервисы для обработки и оптимизации изображений, пример
- Интеграция CI/CD, сборка образа при пуше в выбранную ветку а также автоматическое обновление и перезапуск сервисов
- Создание кластера Kubernetes или Swarm если серверов больше чем 1, для балансировки нагрузки и легкого горизонтального масштабирования
Итого
- Мы успешно опубликовали приложение на сервер и подготовили его к дальнейшему масштабированию.
- Познакомились с docker и получили представление о том как оборачивать свое приложение в контейнер.
- Узнали какие шаги можно совершить далее для улучшения инфраструктуры.
Исходники
Сайт, который показан на скриншотах
Приложение
Конфиги
Благодарю за внимание и надеюсь данный материал вам помог!
Автор: vik_kod