Docker: создаём и запускаем веб приложение



О чём эта статья?

Публикация посвящена созданию, установке и запуску прототипа будущего веб приложения. Легко расширяемого и простого в эксплуатации. В качестве платформы используем решение от Docker.

Для кого она предназначена?

Если вы собираетесь создать сайт, блог или полноценный веб сервис, хотите написать своё первое API (от англ. application programming interface), или в вас живёт дух первопроходца и вы хотите попробовать всё сразу и узнать как можно запустить своё приложение с помощью Docker, то обязательно продолжайте читать!

"Вообще-то я программист" или при чём тут Docker..

Наверняка, многие из читающих слышали, а некоторые уже пробовали применять в создании сервисов решение от компании Docker. Название было выбрана не случайно (docker англ. - "портовый рабочий"), ведь дальше, мы будем оперировать такими терминами, как контейнер (container англ. - "контейнер"), доставка (ship англ. - "доставка по воде") и прочие. Возможно, новичкам будет проще ориентироваться, если они будут представлять именно портовый контейнер, когда мы будет говорить о виртуальных контейнерах. Итак, начнём!
 

10 раз отмерь и один раз запрограммируй!

Прежде чем начать думать о технологии, сперва обсудим архитектуру будущего приложения. В качестве примера для данной статьи, мы будем строить простой веб сервис (сайт), с главной страницей и работающий как одно-страничное приложение (от англ. SAP - single page application). Для того, чтобы сайт стал одно-страничным приложением, мы создадим простой API для взаимодействия главной страницы с сервером.

Какие технологии будем использовать

Будем использовать NodeJS и Nginx. Причина использования - простота и низкий порог входа для того кто хочет попробовать собрать свой первый контейнер. Можно выбрать любой популярный язык программирования.

Проектируем приложение

Поскольку наше приложение - веб приложение, то каждый запрос к нему приходит по HTTP. Каждый из таких запросов будет попадать на подобие маршрутизатора - веб сервер nginx. В зависимости от типа запроса, а в нашем приложении их будет два типа (запрос за статикой и запрос к API), будем использовать либо сервер отдающий статику (статические, неизменяемые данные), либо сервер отдающий динамические данные (меняющиеся со временем данные). Если бы мы решили визуализировать слова, описанные выше, то получился бы следующий набросок:

Изображение будущего контейнера

Перед тем как создать контейнер, необходимо описать изображение (image англ. - образ, изображение) будущего контейнера. Со слепка, который мы составим, Docker Engine сконструирует контейнер. При создании контейнера, под него автоматически будет выделено место в файловой системе. Внутри создастся изолированная среда включая конкретную часть приложения, описанную в изображении, и все его зависимости.
Для нашего приложения и согласно схеме из пункта выше, нам понадобится три образа: NginxMain PageAPI. Каждый из образов - независимая часть общего (отдельное приложение). В сочетании части образуют единое web приложение. Далее, рассмотрим каждый из них в отдельности.

Файловая структура

Одно из главных правил при старте нового проекта - поступать честно и обдуманно с самого начала. Чтобы обозначить структуру приложения так же чётко, как и на бумаге, повторим её в файловой системе:
project_folder/
  nginx/
  web/
  api/

Docker Hub или место где хранятся все образы

Перед тем, как начать создавать первый образ необходимо знать о существовании двух типов образов: базовый образ и образ использующий базовый. Первый является основой, второй расширяет первый. Сообщество пишущих под Docker собрало достаточно обширное множество базовых образов. Аналогично GitHub, для образов существует DockerHub. Публичные образы есть уже у многих зарекомендовавших себя на рынке продуктов. Их можно вытягивать и создавать новые на их основе.

Образ NGINX

В папке nginx создадим файл с названием Dockerfile:
project_folder/
 nginx/
  Dockerfile
  nginx.conf
  ..
Отныне папка, в которой создан Dockerfile, является корнем для будущего образа. Воспользуемся базовым образом для nginx. Команда FROM указывает на необходимость вытянуть соответствующий образ по ключевому слову из общего репозитория. Укажем почту разработчика, занимающегося поддержкой создаваемого образа в строчке MAINTAINER.
Далее, согласно принципам работы Docker, необходимо скопировать единственный файл из папки nginx в то место, которое будет выделено Docker под будущий контейнер командой COPY. Команда WORKDIR установит рабочую директорию внутри контейнера. По умолчанию, контейнер - изолированная среда. EXPOSE выведет наружу 80 порт, для возможности соединения с приложением, запущенном внутри контейнера. Последняя команда CMD выполнит код, для запуска приложения внутри контейнера.
FROM nginx

MAINTAINER your_email_address

COPY nginx.conf /etc/nginx/nginx.conf

WORKDIR /etc/nginx

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
Также, внутри папки nginx необходимо создать nginx.conf с подходящей для вас конфигурацией. Подробное руководство можно найти по ссылке. Один из вариантов конфига приведен ниже:
worker_processes auto;

events {
 worker_connections 8096;
 multi_accept on;
 use epoll;
}

http {

 fastcgi_read_timeout 60;

 sendfile off;
 keepalive_timeout 10;
 keepalive_requests 1024;
 client_header_timeout 10;
 client_body_timeout 10;
 send_timeout 10;

 gzip on;
 gzip_vary on;
 gzip_comp_level 2;
 gzip_buffers 4 8k;
 gzip_proxied expired no-cache no-store private auth;
 gzip_min_length 1000;
 gzip_disable "MSIE [1-6]\.";
 gzip_types text/plain text/xml text/css
 text/comma-separated-values
 text/jаvascript
 application/x-jаvascript
 application/atom+xml;

 # WEB CONTAINER LINK
 upstream web_servers {
 server web:8080;
 }
 # API CONTAINER LINK
 upstream api_servers {
 server api:8080;
 }

 server {

 listen 80;

 location / {

 proxy_pass http://web_servers;
 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-Host $server_name;

 }

 location /api/ {

 proxy_connect_timeout 30s;
 proxy_send_timeout 30s;
 proxy_read_timeout 30s;

 proxy_pass http://api_servers;
 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-Host $server_name;

 }
 }
}
Пример выше я вынес на рассмотрение исключительно из-за возможности продемонстрировать каким образом необходимо указывать ссылки на контейнеры внутри конфига nginx. Как вы видите из примера выше, ссылка на веб контейнер (сервер отдающий статику) определяет соответствующий поток:
 upstream web_servers {
   server web:8080;
 }
Поток на контейнер API определяется другой ссылкой:
upstream api_servers {
  server api:8080;
}
Таким образом, потоки разделены и мы можем приступить к описанию каждого из серверов. Что касается ссылок webapi, используемых внутри конфига nginx, то их определением мы займёмся в разделе "Взаимодействие контейнеров"

Образ WEB (Main Page)

В папке web будем хранить главную страницу нашего приложения и иные статические данные, такие как картинки, шрифты, иконки и прочее:
project_folder/
 web/
  Dockerfile
  app.js
  package.json
  public/index.html
  ..
Аналогично предыдущему образу, опишем образ приложения для статического контента:
FROM node:latest

MAINTAINER your_email_address 

# 1.
WORKDIR /tmp
ADD package.json /tmp/package.json
RUN npm config set registry http://registry.npmjs.org/
RUN npm install
RUN mkdir -p /usr/src/app
RUN cp -a /tmp/node_modules /usr/src/app

WORKDIR /usr/src/app
# 2.

ADD . /usr/src/app

EXPOSE 8080

CMD [ "npm", "run", "prod" ]
Трюк, описанный между пунктами 1. и 2., необходим для быстрой работы с большим количеством зависимостей в NodeJS. В данном случае, команда ADD работает аналогично COPY.
Команда npm run prod описана в файле package.json следующим образом:
NODE_ENV=production node --harmony app.js 
В свою очередь, app.js простой сервер, отдающий index.html:
"use strict";

const express = require('express');
const http = require('http');

const app = express();
const server = http.Server(app);

const path = require('path');

app.use(express.static(path.join(__dirname, '/public')));

app.get('*', (request, response) => {
 response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
});

server.listen(8080);

Образ API

В папке api мы описываем взаимодействие с любыми динамически меняющимися данными:
project_folder/
 api/
  Dockerfile
  app.js
  package.json
  data/..
  ..
В нашем случае образы серверов статического и динамического контента не отличаются. Что касается самого сервера, то код его следующий:
"use strict";

const express = require('express');
const bodyParser = require('body-parser');
const http = require('http');

const app = express();
const server = http.Server(app);

const path = require('path');

const AppRouter = require('./Routers/App.router');

app.use('/api'.concat('/demo'), AppRouter);

server.listen(8080);
Простой маршрутизатор (Router), может выглядеть следующим образом:
const express = require('express');
const router = express.Router();

router.get('/demo', function (req, res, next) {
  res.status(200).json({
    success: true,
    process: 'started',
  });
})

module.exports = router;

Взаимодействие контейнеров

После описания каждого из образов, создадим единый файл, в котором опишем взаимоотношения между будущими контейнерами. Команда Docker предоставляет мощный инструмент для манипуляции и запуска много-контейнерных приложений - Docker Compose.
project_folder/
  docker-compose.yml
  nginx/
  web/
  api/
Compose файл имеет следующую структуру (мы используем лишь малую часть доступного функционала). Указываем каждый из образов отдельным пунктом. Указываем точку входа для каждого из образов (корень, в котором лежит Dockerfile) в атрибуте build. Связи между контейнерами через атрибут links. Атрибут ports указывает на открытые порты, через которые контейнеры взаимодействуют друг с другом и с внешним миром.
nginx:
 build: ./nginx
 links:
 - web:web
 - api:api
 ports:
 - "80:80"

web:
 build: ./web
 ports:
 - "8080"

api:
 build: ./api
 ports:
 - "8080"
Как мы видим, линки прокидываются в контейнер nginx, внутри которого мы сможем использовать ключевые слова (webapi) как ссылки на соответствующие связанные контейнеры. Использование связей рассмотрено более подробно в описании образа nginx.

Предстартовая готовность

Осталось совсем немного для запуска приложения. Создавать и запускать контейнеры можно в разных средах, в том числе и на вашей платформе. Однако, для целей разработки, удобнее использовать виртуальные машины. Для того чтобы запустить Docker Engine на виртуальном хосте, существует специальный инструмент Docker Machine
1. Используя синтаксис инструмента, создадим виртуальную машину на базе VirtualBox:
$ docker-machine create --driver virtualbox vb
До того как выполнить код выше, необходимо установить VirtualBox в свою систему. Достаточно скачать установочный файл с официального сайта и выполнить инсталляцию. 
2. После успешного создания машины, мы сможем увидеть её в списке доступных машин воспользовавшись командой:
$ docker-machine ls
3. Следующим шагом необходимо переключиться в контекст созданной виртуальной машины:
$ eval "$(docker-machine env vb)"
В случае, если машина не запущена, перед переключением контекста, необходимо выполнить команду "старт":
$ docker-machine start vb
4. Увидеть основные параметры запущенной виртуальной машины возможно использовав команду:
$ docker-machine env vb
Здесь же будет указан IP адрес и порт, по которым будет доступен запущенный экземпляр приложения.

 

Старт!

Дело за малым. Осталось создать и запустить будущие контейнеры. Вся работа, проделанная выше, позволяет осуществить старт одной командой в корневой директории проекта project_folder:
$ docker-compose up
Контейнеры последовательно соберутся на основе образов, свяжутся между собой в единое приложение на основе compose файла и стартуют как единое приложение на виртуальной машине.
Добавить комментарий

Оставить комментарий