пятница, марта 20, 2015

Разворачиваем приложение на Erlang (cowboy) с помощью Docker.

note: делалось это не для production кода, а с целью самообучения. Некоторые решения могут быть странными. Это получилось от того, что с докером я познакомился буквально на днях. Если кому что-то будет резать глаз - не поленитесь в комментариях написать про ошибку и как сделать правильно, спасибо. Также предполагается, что вы знаете зачем нужен docker.

1. Структура проекта.

Под каждый контейнер выделяю отдельную папку. В корне проекта положим docker-compose.yml (настройки бывшего fig, нынешнего docker-compose).

Получаем такое дерево, в каждом каталоге лежит свой Dockerfile:

  • back - наше erlang приложение
  • front - статические файлы и javascript, в моём случае это React-приложение, которое собирается с помощью webpack.
  • nginx - конфиги для nginx, который будет работать как reverse proxy.
  • pg - скрипты для создания базы данных в postgresql
  • redis - контейнер с redis.
  • sources - контейнер с исходниками.

2. Первый контейнер, sources.

Мне хочется использовать все эти контейнеры для разработки, поэтому мне нужно чтобы исходники проектов внутри контейнеров были теми исходниками, с которыми я работаю, а не их копией. Поэтому создаю контейнер sources, в который через docker-compose будут подключены папки с сырцами.

(Здесь и далее gist с docker-compose.yml это кусочек общего большого файла.)

sources:
build: ./sources/
volumes:
- ./back/:/app/backend
- ./front/:/app/frontend
- ./pg/:/app/pg
FROM phusion/baseimage:0.9.16
CMD ["/sbin/my_init"]
view raw Dockerfile hosted with ❤ by GitHub

3. Второй контейнер, pg.

От postgres мне пока требуется всего две вещи: работать с заданным мною паролём и хранить данные в указанной папке. Для этого возьмём docker image из стандартного репозитория. В нём оба вопроса решаются переменными окружения.

pg:
build: ./pg/
ports:
- "5432:5432" # по идее, в данном случае можно не указывать, но так очевиднее
volumes:
- /var/lib/pgdata:/var/pgdata # в Dockerfile пути в разделе volume только внутри папки с docker-context, потому задаём их "снаружи"
environment:
POSTGRES_PASSWORD: mypass1234 # про переменные читаем в описании image: https://registry.hub.docker.com/_/postgres/
PGDATA: /var/pgdata
FROM library/postgres:9.4
view raw Dockerfile hosted with ❤ by GitHub

4. Третий контейнер, redis.

С редисом всё просто до неприличия. Мне даже неинтересно, где он данные разместит и что с ними будет после перезапуска. (Что и как настроить в противном случае, описано здесь)

redis:
build: ./redis/
ports:
- "6379:6379" # опять же, по умолчанию image redis делает expose порта 6379, поэтому указывать его здесь не обязательно, если мы не хотим его перенаправить.
FROM redis:2.6
view raw Dockerfile hosted with ❤ by GitHub

5. Четвёртый контейнер, backend.

Тут у меня будет строиться и стартовать erlang-приложение. Исходники приложения будут лежать в /app/backend. Приложение представляет из себя rest и вебсокет хэндлеры на cowboy. Если мне захочется чтобы исходники перегружались на лету в процессе разработки, я подключу в зависимостях пакет sync или live.

backend:
build: ./back/
volumes_from:
- sources
links:
- pg # таким образом в контейнере backend будет прорва переменных окружения, содержащих в себе всю необходимую информацию о подключении
- redis # к postgres и redis (ip-адреса и порты)
ports:
- "8080:8080"
- "8443:8443" # на этих портах у меня висит cowboy
from unbalancedparentheses/erlang:17.4
WORKDIR /app/backend
CMD sh -c "./run_with_docker.sh; while true; do sleep 10; done"
# самый сомнительный момент. Но эрланг-консоли докеру недостаточно, чтоб не грохнуть контейнер
view raw Dockerfile hosted with ❤ by GitHub
#!/bin/sh
cd /app/pg
./create.sh # идемпотентные скрипты создания базы
./upgrade.sh # и инкрементального апгрейда схемы до текущей версии. Использую свой велосипед https://github.com/kucheruk/pgup
cd -
make # строим релиз с erlang.mk
_rel/my_app_release/bin/my_app_release start
while ! tail -f _rel/my_app_release/log/erlang.log.1 ; do sleep 3; done # файл с логом появится не сразу

6. Пятый контейнер, frontend.

С фронтом всё совсем просто: запускаем webpack в режиме watch, чтобы построив bundle он висел дальше, слушал изменения и достраивал по необходимости.

frontend:
build: ./front/static
volumes_from:
- sources
environment:
- WEBPACK_PARAMS
ports:
- "8090:8090" # вебпак можно запустить с дев-сервером, который с помощью вебсокетов умеет перегружать страницу после изменений в коде.
FROM node:0.10
RUN npm install -g webpack
WORKDIR /app/frontend/static
VOLUME /app/frontend/static/node_modules/
CMD npm install && webpack $WEBPACK_PARAMS --optimize-dedupe --optimize-occurence-order --progress --color --watch
# на какой-то машине можно выставить переменную WEBPACK_PARAMS=--optimize-minimize, например
view raw Dockerfile hosted with ❤ by GitHub

7. Шестой контейнер, nginx.

Теперь нам нужно поставить на входе nginx, который примет соединение и отправит его в зависимости от типа либо к бэкенду, либо на статический файл. Вопрос лишь в том, как nginx'у узнать адреса серверов. Проблема в том, что переменные окружения в nginx можно использовать только в главном конфиге, но нельзя в модулях, например upstream и location. Поэтому сделаем так, чтобы на запуске контейнера с nginx на лету формировался файл с конфигом:

nginx:
build: ./nginx/
volumes_from:
- sources
ports:
- "80:80"
links:
- backend
- frontend
FROM nginx
ADD proxy.conf /etc/nginx/conf.d/proxy.conf
ADD run.sh ./
EXPOSE 80
EXPOSE 443
CMD sh run.sh
view raw Dockerfile hosted with ❤ by GitHub
server {
listen: 80;
index index.html;
location /auth/ {
access_log off;
proxy_pass http://erl;
proxy_set_header HOST $host;
# если не поменять заголовок HOST, cowboy будет уверен, что его спрашивают по адресу http://erl, что в некоторых случаях приведёт к проблемам.
proxy_set_header X-Forwarded-For $remote_addr;
}
location /ws/ {
access_log off;
proxy_pass http://erl/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
location / {
access_log off;
index index.html;
alias /app/frontend/;
}
}
view raw proxy.conf hosted with ❤ by GitHub
#!/bin/sh
(echo "upstream erl { server $BACKEND_PORT_8080_TCP_ADDR:$BACKEND_PORT_8080_TCP_PORT; }" && cat /etc/nginx/conf.d/proxy.conf) > proxy.conf.new
mv proxy.conf.new /etc/nginx/conf.d/proxy.conf
rm /etc/nginx/conf.d/default.conf
nginx -t
nginx
while true; do sleep 10; done;
view raw run.sh hosted with ❤ by GitHub

8. Всё готово.

docker-compose build

docker-compose up

При необходимости прицепиться к запущенным контейнерам и посмотреть логи, пишем docker-compose logs. Если требуется что-то выполнить внутри запущенного контейнера, пишем docker ps, смотрим название контейнера в правом столбце "NAMES", например myapp_nginx_1. И выполняем: docker exec myapp_nginx_1 ls -lR /etc/nginx/


Комментариев нет: