
В средних и больших проектах сайт не ограничивается одним сервисом — к примеру только сайтом, как правило существует база данных, 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"]
Dockerfile
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
После того как образ успешно загрузился можно приступить к конфигурации VPS сервера,
у моего она следующая:
- 1 ГБ оперативной памяти
- 4 ядра
- 30 ГБ диск
Также я воспользовался возможностью поставить docker сразу при создании сервера, поэтому если на вашем VPS он не установлен, инструкцию можно почитать на официальном сайте.
После создания сервера заходим на него и логинимся в 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 небольшими изменениями.
nginx.conf
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 ...
Обновленный 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/ # Монтируем папку со статикой - ./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; }
Обновленный nginx.conf
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 и получили представление о том как оборачивать свое приложение в контейнер.
- Узнали какие шаги можно совершить далее для улучшения инфраструктуры.
Исходники
Приложение
Конфиги
Благодарю за внимание и надеюсь данный материал вам помог!
