Деплой Rails-приложения с помощью Docker

Автор оригинала: ichiel Sikkes
  • Перевод

Введение


Эта запись о том, как я деплоил Ruby On Rails-приложение на сервер DigitalOcean, чтобы оно работало в отдельном Docker-контейнере. Для простоты, я собираюсь очень подробно объяснить процесс развертки Rails-приложения внутри Docker-контейнера
В этом посте:
  • Как я установил Docker на сервере
  • Dockerfile для моего Rails-приложения
  • Сборка с гемами из Gemfile
  • Сборка со скомпилированными ассетами
  • Запуск приложения в Docker
  • Переменные окружения Docker для database.yml

Давайте начнем с установки на сервере.

Установка Docker на сервере


Первым делом я загрузил новый Ubuntu 14.04 на DigitalOcean и установил Docker:
workstation $ ssh root@178.62.232.206
server $ apt-get install docker.io
server $ docker -v
Docker version 1.0.1, build 990021a

Dockerfile и nginx.conf


Теперь нам нужно собрать Docker-образ из Rails-приложения. Так получилось, что Jeroen (Jeroen van Baarsen, прим. перев.) написал об этом на прошлой неделе: Как я собрал Docker-образ для Rails-приложения. Я буду использовать его пост в качестве основы для дальнейших шагов.
Я буду собирать образ на том же сервере, на котором хочу впоследствии хостить само приложение. Я решил сделать так, потому что хочу, чтобы приложение не было в публичном доступе, поэтому публичный Docker-репозитарий — плохой вариант для этого. Я бы мог настроить для себя приватный репозитарий, но тогда я должен был бы поддерживать его, чего я не хочу делать в данный момент. В этом посте, я рассматриваю самый простой способ использования Docker для хостинга приложения.
В свой проект intercity-website я добавил следующие конфигурационные файлы Dockerfile и nginx.conf:

Dockerfile

FROM phusion/passenger-ruby21
MAINTAINER Firmhouse "hello@firmhouse.com"

ENV HOME /root
ENV RAILS_ENV production

CMD ["/sbin/my_init"]

RUN rm -f /etc/service/nginx/down
RUN rm /etc/nginx/sites-enabled/default
ADD nginx.conf /etc/nginx/sites-enabled/intercity_website.conf

ADD . /home/app/intercity_website
WORKDIR /home/app/intercity_website
RUN chown -R app:app /home/app/intercity_website
RUN sudo -u app bundle install --deployment
RUN sudo -u app RAILS_ENV=production rake assets:precompile

RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

Как можно заметить, Dockerfile использует базовый образ phusion/passenger-ruby21. Он добавляет конфиг Nginx, код приложения, запускает bundler для установки gem'ов а также прекомпилирует ассеты.

nginx.conf

# This is the server block that serves our application.
server {
  server_name intercityup.com;
  root /home/app/intercity_website/public;

  passenger_enabled on;
  passenger_user app;
  passenger_ruby /usr/bin/ruby2.1;
}

# This is the server block that redirects www to non-www.
server {
  server_name www.intercityup.com;
  return 301 $scheme://intercityup.com$request_uri;
}

Сборка образа для контейнера приложения


Я добавил эти файлы в мой репозитарий. Теперь я собираюсь загрузить его на сервер и собрать контейнер:
my_workstation $ git archive -o app.tar.gz --prefix=app/ master
my_workstation $ scp app.tar.gz root@178.62.232.206:
my_workstation $ ssh root@ 178.62.232.206
server $ tar zxvf app.tar.gz
server $ docker build --tag="intercity-website" app/

Эта команда выводит много результатов и делает много всего. Когда я в первый раз я запустил docker build, это заняло несколько минут. Это потому, что Docker должен скачать базовый образ phusion/passenger-ruby21. Это делается только один раз. После загрузки базового образа процесс продолжится в соответствии с моим Dockerfile.

Теперь команда docker images показывает мой образ:
server $ docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
intercity-website          latest              629f05f42915        3 minutes ago       1.011 GB

Запуск контейнера в первый раз


Настало время запустить приложение.
server $ docker run --rm -p 80:80 intercity-website

Эта команда запускает контейнер, выводит некоторые данные, и, наконец, выводит следующую строку:
[ 2014-09-23 11:23:11.9005 113/7fb22942b780 agents/Watchdog/Main.cpp:728 ]: All Phusion Passenger agents started!

Теперь давайте посмотрим, корректно ли работает приложение. Выполним запрос с помощью curl:
server $ curl -H "Host: intercityup.com" http://localhost/
<!DOCTYPE html>
<html>
<head>
  <title>We`re sorry, but something went wrong (500)</title>
...

Упс, что-то пошло не так. Судя по логу, находящийся внутри контейнера, (для доступа в который я использовал docker-bash от Phusion), я забыл создать базу данных. Так что теперь я собираюсь установить на сервере MySQL.

Установка базы данных


Я буду использовать стандартный сервер MySQL, доступный в Ubuntu 14.04:
server $ apt-get install mysql-server

После установки, и задания пароля администратора, я могу создать базу данных для приложения:
server $ mysql -u root -p
mysql> create database intercity_website_production;
Query OK, 1 row affected (0.00 sec)
mysql> grant all on intercity_website_production.* to 'intercity' identified by 'rwztBtRW6cFx9C';
Query OK, 0 rows affected (0.00 sec)

После этого я изменил /etc/mysql/my.cnf а также bind-address с 127.0.0.1 на мой внешний IP адрес, 178.62.232.206. Таким образом, Rails в моем контейнере теперь может использовать MySQL. В /etc/mysql/my.cnf я заменил строку с bind-address на следующую:
bind-address        = 178.62.232.206

И перезапустил MySQL:
server $ /etc/init.d/mysql restart


Использование переменных окружения для настройки базы данных


Я собираюсь использовать переменные окружения, чтобы мой контейнер мог использовать их для авторизации в MySQL. Чтобы сделать это, мне нужно сделать две вещи: 1) Подготовить database.yml файл в репозитории к использованию переменных окружения. и 2) настроить Nginx для передачи этих переменных в процесс passenger'a.

Вот мой новый database.yml, подготовленный для переменных окружения:
production:
  adapter: mysql2
  host: <%= ENV['APP_DB_HOST'] %>
  port: <%= ENV['APP_DB_PORT'] || "3306" %>
  database: <%= ENV['APP_DB_DATABASE'] %>
  username: <%= ENV['APP_DB_USERNAME'] %>
  password: <%= ENV['APP_DB_PASSWORD'] %>

Чтобы эти переменные окружения работали для моего Rails-приложения, мне нужно настроить Nginx. Это обусловлено тем, что Nginx сбрасывает все переменные окружения, за исключением тех, которые вы определяете.

Я добавил в Rails-приложение файл rails-env.conf:
env APP_DB_HOST;
env APP_DB_PORT;
env APP_DB_DATABASE;
env APP_DB_USERNAME;
env APP_DB_PASSWORD;

А также поправил Dockerfile, чтобы он добавлял файл rails_env при сборке контейнера:
FROM phusion/passenger-ruby21
MAINTAINER Firmhouse "hello@firmhouse.com"

ENV HOME /root
ENV RAILS_ENV production

CMD ["/sbin/my_init"]

RUN rm -f /etc/service/nginx/down
RUN rm /etc/nginx/sites-enabled/default
ADD nginx.conf /etc/nginx/sites-enabled/intercity_website.conf
# Add the rails-env configuration file
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf

ADD . /home/app/intercity_website
WORKDIR /home/app/intercity_website
RUN chown -R app:app /home/app/intercity_website
RUN sudo -u app bundle install --deployment
RUN sudo -u app RAILS_ENV=production rake assets:precompile

RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

EXPOSE 80


Построение образа с поддержкой переменных окружения


Я добавил в репозитарий новый конфиг Nginx. Теперь я намерен пересобрать новую версию контейнера:
workstation $ git archive -o app.tar.gz --prefix=app/ master
workstation $ scp app.tar.gz root@178.62.232.206:
workstation $ ssh root@178.62.232.206
server $ tar zxvf app.tar.gz
server $ docker build --tag="intercity-website" app/

Запуск rake с переменными окружения


После сборки контейнера я могу настроить базу данных. В следующей команде я использую переменные окружения для передачи информации о подключении к базе данных для запуска rake db:setup. Обратите внимание, что я добавил к команде аргумент -u app. Этот аргумент нужен, чтобы удостовериться, что rake db:setup запускается от имени пользователя app внутри контейнера.
server $ docker run --rm -e "RAILS_ENV=production" -e "APP_DB_HOST=178.62.232.206" -e "APP_DB_DATABASE=intercity_website_production" -e "APP_DB_USERNAME=intercity" -e "APP_DB_PASSWORD=rwztBtRW6cFx9C" -e "APP_DB_PORT=3306" -u app intercity-website rake db:setup

intercity_website_production already exists
-- create_table("invite_requests", {:force=>true})
   -> 0.0438s
-- initialize_schema_migrations_table()
   -> 0.1085s

Ух-ты! Сработало!


Запуск приложения с переменными окружения


Теперь я могу запустить контейнер с теми же переменными окружения и попытаться получить доступ к нему из браузера, чтобы проверить, работает ли он:
server $ docker run --rm -p 80:80 -e "RAILS_ENV=production" -e "APP_DB_HOST=178.62.232.206" -e "APP_DB_DATABASE=intercity_website_production" -e "APP_DB_USERNAME=intercity" -e "APP_DB_PASSWORD=rwztBtRW6cFx9C" -e "APP_DB_PORT=3306" intercity-website

Когда я открываю 178.62.232.206, я вижу Rails-приложение, которое подключается к базе данных, и также вижу, ассеты был скомпилированы и все работает. Победа!

Заключение


На этом мы завершаем пост, где мы:
  1. Установили Docker на сервере
  2. Настроили Dockerfile и построили образ контейнера
  3. Настроили базу данных с помощью переменных окружения

Что дальше?


У меня до сих пор есть вопросы, требующие ответа. Я и другие разработчики в Intercity ещё будем писать о них. Вот некоторые из вопросов, которые нужно решить:
  • Как автоматизировать развертывание? Может быть использовать что-то вроде Capistrano?
  • Что мне нужно, чтобы получить нулевое время простоя? Когда я сперва остановлю, а потом запущу контейнер, для запуска новой версии приложения, подключения будут сброшены.
  • Где хранить переменные окружения для каждого из приложений, которые я собираюсь развернуть на сервере?
  • Как мне ускорить построение контейнера? Нужно ли мне запускать каждый раз bundler и rake assets:precompile для каждого деплоя?

Я надеюсь, что вам понравился этот пост. Буду рад советам и вопросам!

Большое спасибо за внимание.
Поделиться публикацией

Комментарии 20

    0
    p.s. Спасибо freeman1988 за вычитку наскоро переведённого материала.
      +4
      У автора получился фарш из всего подряд в одном контейнере, а часть еще и на хост машине — это противоречет идеалогии docker. По-моему должно было получиться так: 1 конейнер — nginx, 2 — mysql, 3 — приложение.
        +3
        Да, статья выглядит даже вредной — помимо фарша, еще заметил, что мускуль смотрит наружу, что очень не рекомендуется делать, в Dockerfile в конце бессмысленная команда удаления кеша apt и временных файлов, которая на размер контейнера не влияет.
          0
          Эта проблема тянется из phusion, которые намешали такую кашу в свой шаблон.
          0
          А вообще как лучше хранить БД? Я так понимаю желательно создать для СУБД отдельный контейнер, а куда сами данные девать? Держать в том же контейнере, на хостовой машине или может отдельный контейнер создавать?
            +1
            Я храню на хост машине, как и логи. Для этого в докере у run есть параметр -v, а в докерфайлах VOLUME. Как это сделать в каком-нибудь кластере CoreOS пока не догадываюсь, ведь там нужно учесть то, что контейнер может запускаться на разных нодах, в зависимости от их доступности.
              0
              Отдельный контейнер для СУБД, отдельный контейнейр чисто для VOLUME файлов данных. Контейнер СУБД юзает volume из контейнера данных. Если надо бэкапиться — запускаете отдельный контейнер, который юзает этот же volume из контейнера данных, и внутри этого нового контейнера запускаете бэкап.
              0
              А гему при каждом деплое будут заново устанавливаться?
                0
                Можно по разному сделать — если вы уверены, что важи гемы не будут меняться их можно загнать в базовый docker-образ, и тогда деплой будет оч. быстный, если нет — тогда да, будут каждый раз устанавливаться.
                  0
                  А можно сделать чтобы bundle install работал как обычно. Устанавливал только те гему, которые изменились?
                    0
                    Так можно сделать, но это опять же противоречит идеологии докера. По задумке, при каждом деплое конейнер пересоздается, это (а так же то, что он выполняет только одну задачу) позвол держать его чистым, незагаженным, имеющим всегда одно и то же контролируемое окружение.
                      0
                      Понятно, а есть ли какой-то способ не противоречить идеологии докера и иметь возможность деплоить быстро? Например, если мне нужно сделать хотфикс я не хочу ждать, пока докер устанавлилвает десятки гемов и компилирует native extensions.
                        0
                        Может быть кто-то другой ответит. Я просто nodejs-стек использую — там установка npm-пакетов достаточно быстро происходит. Не пытался это как-то оптимизировать.
                          0
                          Есть — собирать новый образ (долго), после чего останавливать запущенный контейнер (быстро) и тут же запускать его по-новой на уже собранном новом образе (так же быстро). Итого имеем очень маленький даунтайм
                    +1
                    Прикол докера в том, что после каждой команды Dockerfile результирующий контейнер кешируется. Т.е. если у вас был такой Dockerfile
                    RUN apt-get update
                    RUN apt-get install package1 package2
                    

                    а поменялся на такой
                    RUN apt-get update
                    RUN apt-get install package1 package2 package3
                    

                    то при сборке следующей он возьмет тот контейнет, который создался в прошлый раз после apt-get update и накатит на него apt-get install package1 package2 package3. Вы можете сделать по-другому:
                    RUN apt-get update
                    RUN apt-get install package1 package2
                    RUN apt-get install package3
                    

                    Тогда при сборке он возьмет состояние после RUN apt-get install package1 package2 и накатит на него только apt-get install package3.

                    Если у вас было
                    RUN command1
                    ADD directory or file
                    RUN command2
                    

                    то при следующей сборке docker посчитает хеш добавленных в ADD файлов, и будет выполнять RUN command2 только если в них что-то поменялось. Т.о. я прихожу к такому предпочитаемому формату Dockerfile

                    RUN install and configure all server software (типа поставить интерпретатор и fcgi-демон для нужной технологии)
                    ADD vendors config (у нас в PHP это composer.json + composer.lock, в nodejs это будет package.json в каждой технологии свои)
                    RUN install vendors (composer install, npm install etc)
                    ADD static files (js, img, css, scss etc)
                    RUN compile static files (minify js css, etc)
                    ADD all source code
                    


                    Т.о. получается, что при небольшом изменении кода, если это не затрагивает статику и вендоров, контейнер будет билдиться практически моментально (и соответственно деплоиться тоже, т.к. при деплое докер загрузит только изменения, связанные с добавлением других source файлов). Если вы измените JS, то билд будет немного дольше — нужно будет перекомпилировать все JS. И только если измените список вендоров, вам придется перекачивать этих вендоров заново.
                    +1
                    Кстати, а зачем вообще, деплоить rails приложение в докер?
                      0
                      Изолированность, переносимость, масштабируемость. Смысл примерно такой же, как и в том зачем программы делить на слабосвязные модули/функции и т.д. Посто таким образом можно строить более сложные системы.
                        +2
                        В данном методе профита вообще никакого. Как писали выше — только вред.
                        Кроме того, самое интересное — как деплоить и откатываться автоматически аналогично капистрано — не раскрыто.
                        Ну и гзшечками код раскидывать — тоже не лучший вариант. Привычнее git и cached copy в контейнере или volume.
                        Переносимость — можно использовать chef. Масштабируемость вообще к докеру отношения не имеет.
                        Спасибо, что перевели статью. Статью без лучших практик и с нарушением их.
                      0
                      del
                        0
                        Лучше использовать doker для контениризации, а деплоить Rails-приложения с помощью capistrano.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое