В средних и больших проектах сайт не ограничивается одним сервисом? —? к примеру только сайтом, как правило существует база данных, 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 и получили представление о том как оборачивать свое приложение в контейнер.
  • Узнали какие шаги можно совершить далее для улучшения инфраструктуры.

Исходники


Сайт, который показан на скриншотах
Приложение
Конфиги

Благодарю за внимание и надеюсь данный материал вам помог!

Комментарии (6)


  1. buldezir
    04.02.2019 18:25

    довольно часто встречается «ошибка», но mkdir для APP_ROOT не нужен

    The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile. If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.


    1. vik_kod Автор
      04.02.2019 18:34

      Поправил, спасибо!


  1. munrocket
    04.02.2019 18:57

    Поздравляю нового хаброжителя :)


  1. dipiash
    04.02.2019 20:02

    Хорошая статья, спасибо!


    А что насчёт следующих моментов:
    1) перезапуск
    приложения или контейнера при падении
    2) graceful reload при деплое новой версии приложения
    Могли бы вы поделиться своим видением как это делать?


    1. vik_kod Автор
      04.02.2019 21:31

      Приветствую!

      На случай падения я бы делал health-check контейнеров и держал запасной контейнер для такого сценария. Для бесшовного же обновления билдил образ на отдельном сервере(обычно это CI). После чего на продакшн сервере вытягивал обновленный, запускал, а старый останавливал и удалял.

      Более опытные товарищи могут меня поправить если я не прав.


  1. tonyvolcano
    06.02.2019 11:13

    А кто мог бы подсказать как настроить этот же проект под Windows Server 2016? не под nginx а под IIS.