Docker для приложения Rails 7

Введение

Широкое распространение развертывания приложений с использованием Docker стало причиной написания этой статьи.

Преимущества, недостатки, сложности и прочие сравнительные аспекты широко освещаются в различных руководствах, являются причинами создания различных по сложности и наполненности курсов, обучающих материалов и т.д. и т.п.

Попробуем подойти к этому вопросу с практической стороны и решить задачу без наличия каких либо специфичных знаний в этой области.

В качестве исходных данных возьмем следующее:

  • домашний ноутбук с операционной системой Mac OS Big Sur

  • работающее приложение на Rails 7

  • используемую базу данных postgres

➜  ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin20]
➜  portfolio git:(master) pg_ctl -V
pg_ctl (PostgreSQL) 14.7 (Homebrew)

Разобьем задачу на этапы:

  1. Установка Docker

  2. Перенос базы PostgreSQL в контейнер.

  3. Подключение контейнера к работающему приложению.

  4. Перенос приложения в контейнер

  5. Подключение приложения из контейнера к контейнеру с базой данных.

  6. Использование возможностей Docker для автоматизации данного процесса.

  7. Посмотрим, что получается и что можно сделать дальше.

Установка Docker

С этим пунктом все просто. Если операционная система и железо не "старое", получается быстро и буквально по инструкции.

Скачиваем уже предлагаемый пакет Docker и устанавливаем его. Следуем инструкции Install and run Docker Desktop on Mac. Отличная инструкция на русском языке есть на habr Полное практическое руководство по Docker Там же приведена терминология и описаны основные моменты работы с Docker.

Для доступа к существующим контейнерам потребуется учетная запись на Docker Hub

Проверяем, что после установки все работает.

% portfolio git:(master) docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

На этом установку можно считать завершенной.

Делаем контейнер с PostgreSQL

Поскольку планируем работать с docker в основном из терминала с претензией на более универсальный подход, для удобства используем zsh-docker-aliases.

- dk=docker
- dkr='docker run'
- dkIb='docker image build'
- dke='docker exec'
- dkIls='docker image ls'
- dkpl='docker pull'

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

Дальше по тексту будут использоваться alias из этого plugin

Найдем необходимый нам image PostgreSQL

➜ dk search postgres
NAME                               DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
postgres                           The PostgreSQL object-relational database sy…   12115     [OK]
bitnami/postgresql                 Bitnami PostgreSQL Docker Image                 183
...

Берем первый, он же "официальный", поскольку сейчас особых каких то требований нет и идем по пути наименьшего сопротивления.

➜ dkpl postgres
Using default tag: latest
latest: Pulling from library/postgres
f1f26f570256: Pull complete
...
Digest: sha256:5a90725b3751c2c7ac311c9384dfc1a8f6e41823e341fb1dceed96a11677303a
Status: Downloaded newer image for postgres:latest
docker.io/library/postgres:latest

Запустим postgres instance на основе этого image в detached mode (-d) с открытием всех публичных портов со случайным mapping (-P) Зададим пароль пользователю postgres, чтобы можно было проверить работу из консоли. Можно указать опцию (--rm) для удаления контейнера после завершения работы (dk stop)

dk run --rm -P --name db-primary -e POSTGRES_PASSWORD=password -d postgres

Получаем информацию о запущенных контейнерах

% dkls
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS                     NAMES
27c4bf1a4162   postgres   "docker-entrypoint.s…"   8 seconds ago   Up 7 seconds   0.0.0.0:32771->5432/tcp   db-primary

Берем назначенный порт и подключаемся к базе.

% psql postgresql://postgres:password@localhost:32771
psql (14.7 (Homebrew), server 15.2 (Debian 15.2-1.pgdg110+1))
WARNING: psql major version 14, server major version 15.
         Some psql features might not work.
Type "help" for help.

postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
(3 rows)

postgres=#

Все достаточно просто и работает. Теперь зафиксируем порт для использования в настройках Rails. Остановим контейнер и запустим с определенным портом. Чтобы избежать настроек с безопасностью, возьмем, например порт 54320

dkr --rm -p 54320:5432 --name db-primary -e POSTGRES_PASSWORD=password -d postgres

Переключаем приложение на использование контейнера с postgres

# config/database.yml
default: &default
    adapter: postgresql
    encoding: utf-8
    # collation: ru_RU.UTF-8
    # ctype: ru_RU.UTF-8
    # For details on connection pooling, see Rails configuration guide
    # https://guides.rubyonrails.org/configuring.html#database-pooling
    host: localhost                       # HOST
    port: 54320                           # Port
    username: postgres                    # User Name
    password: password # Password
    pool: <%= ENV.fetch('RAILS_MAX_THREADS', 5) %>

Создаем базу, применяем миграции.

rails db:create db:migrate
Created database 'problems_development'
Created database 'problems_test'
== 20221029170027 CreateProblems: migrating ===================================
-- create_table(:problems)

Проверяем, что получилось.

% psql postgresql://postgres:password@localhost:54320
postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
(3 rows)

Установки locale в образе только по умолчанию. Поправить это можно двумя способами, описано вот здесь Locale Customization.

  • взять образ на базе alpine и указать параметры locale в строке запуска

  • дополнить существующий образ.

Выбираем второй вариант, возможно потом будут еще какие то дополнения.

Создаем Dockerfile

FROM postgres:latest
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
ENV LANG ru_RU.utf8

Создаем image на основе этого файла.

% dkIb .
[+] Building 3.3s (7/7) FINISHED
 => [internal] load build definition from Dockerfile                                                                                            0.0s => => transferring dockerfile:
 => [internal] load metadata for docker.io/library/postgres:latest                                                                              3.1s
 => [auth] library/postgres:pull token for registry-1.docker.io                                                                                 0.0s
 => [1/2] FROM docker.io/library/postgres:latest@sha256:5a90725b3751c2c7ac311c9384dfc1a8f6e41823e341fb1dceed96a11677303a                        0.0s
 => => resolve docker.io/library/postgres:latest@sha256:5a90725b3751c2c7ac311c9384dfc1a8f6e41823e341fb1dceed96a11677303a                        0.0s
 => CACHED [2/2] RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8                                               0.0s
 => exporting to image                                                                                                                          0.0s
 => => exporting layers                                                                                                                         0.0s
 => => writing image sha256:5d99017051a7f0d73cb257b912a9ca3bf334fcfcb8901e442b730fc2dc259840                                                    0.0s
%  portfolio git:(master) ✗ dki
REPOSITORY               TAG       IMAGE ID       CREATED        SIZE
<none>                   <none>    5d99017051a7   2 hours ago    382MB
ubuntu                   latest    08d22c0ceb15   3 weeks ago    77.8MB
docker/getting-started   latest    3e4394f6b72f   3 months ago   47MB

# Переименуем созданный image

%  portfolio git:(master) ✗ dkIt 5d99017051a7 as/db-primary

%  portfolio git:(master) ✗ dki
REPOSITORY               TAG       IMAGE ID       CREATED        SIZE
as/db-primary            latest    5d99017051a7   2 hours ago    382MB
ubuntu                   latest    08d22c0ceb15   3 weeks ago    77.8MB
docker/getting-started   latest    3e4394f6b72f   3 months ago   47MB

Создаем контейнер на основе этого image и проверяем установку locale

% dkr --rm -p 54320:5432 --name db-primary -e POSTGRES_PASSWORD=password -d as/db-primary

%  psql postgresql://postgres:password@localhost:54320

postgres=# \l
                                       List of databases
         Name         |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
----------------------+----------+----------+-------------+-------------+-----------------------
 postgres             | postgres | UTF8     | ru_RU.utf8  | ru_RU.utf8  |
...

Теперь у нас есть контейнер, который создается с использованием нашего Dockerfile c необходимыми нам параметрами locale

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

Перенос приложения в контейнер.

У нас приложение Rails, для него требуется в качестве основы контейнер, который включает в себя web server и сервер приложений, умеющий работать с Rails. Поскольку в "безконтейнерном" варианте для решения данной задачи можно использовать passenger в сочетании с nginx, поищем образ, представляющий базовую конфигурацию для этого. В качестве альтернативного решения возможно использование Universal Web App Server, который тоже существует в образах docker nginx/unit

По размерам примерно одинаковые, возьмем версию с tag 2.3.0, которая использует ruby 3.1.2 по умолчанию, поскольку приложение Rails создано с использованием этой версии.

% dk search passenger
NAME                                    DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
phusion/passenger-full                  Base image for Ruby, Python, Node.js and Met…   113
phusion/passenger-nodejs                Base image for Node.js and Meteor web apps      52
...

Подробное описание настроек phusion / passenger-docker В последующем можно заняться оптимизацией, поскольку полная версия кроме ruby поддерживает python, node и meteor.

Сначала сделаем отдельный image для приложения. Сделаем новый файл Dockerfile.ruby, ниже объединим с созданием контейнера для СУБД PostgreSQL в один процесс с использованием docker-compose.

Берем за основу предлагаемый в описании файл конфигурации Dockerfile и вносим небольшие изменения и дополнения. Немного снабдил комментариями, остальное все достаточно понятно.

# Dockerfile.ruby
FROM phusion/passenger-ruby31:2.3.0
# Set correct environment variables.
ENV HOME /root

# Use baseimage-docker's init process.
CMD ["/sbin/my_init"]

# Enable NGINX
RUN rm -f /etc/service/nginx/down
RUN rm /etc/nginx/sites-enabled/default
# Добавляем конфигурацию NGINX и passenger для приложения.
ADD webapp.conf /etc/nginx/sites-enabled/webapp.conf
RUN mkdir /home/app/webapp

# Config nginx. Можно создать файл конфигурации и включить его в контейнер.
# ADD secret_key.conf /etc/nginx/main.d/secret_key.conf
# ADD gzip_max.conf /etc/nginx/conf.d/gzip_max.conf

# Ruby 3.1.2
# Остальное не используем, поскольку взяли уже образ с необходимой версией по умолчанию.
# RUN rvm install 'ruby-3.1.2'
# RUN bash -lc 'rvm --default use ruby-3.1.2'
RUN ruby -v
RUN rm -f /etc/service/sshd/down

## Install an SSH of your choice.
# Добавляем возможность входа по ssh для этого контейнера. В локальной конфигурации это не требуется,
# можно использовать команду докера для доступа в контейнер (dke -t -i portfolio-db-1 bash -l, например)
# Может быть полезно при размещении контейнера при deploy, когда доступ к базовому хосту ограничен или не возможен.
# Авторизация предусмотрена по ключам, этим и воспользуемся.
ADD id_ed25519.pub /tmp/id_ed25519.pub
RUN cat /tmp/id_ed25519.pub >> /root/.ssh/authorized_keys && rm -f /tmp/id_ed25519.pub

# This copies your web app with the correct ownership.
COPY --chown=app:app ./ /home/app/webapp
ENV HOME /home/app/webapp
WORKDIR $HOME/
COPY Gemfile* $HOME/
# При создании образа будут предупреждения о запуске bundler от имени root. Отключаем.
RUN bundle config --global silence_root_warning 1
RUN bash -lc 'bundle install'

Создадим файл конфигурации для nginx и passenger, используем предлагаемый прототип.


server {
    listen 80;
    server_name mba1.local;
    root /home/app/webapp/public;
    passenger_enabled on;
    passenger_ruby /usr/local/rvm/gems/ruby-3.1.2/wrappers/ruby;
    passenger_user app;
    passenger_app_env development;
    passenger_min_instances  1;

}

Единственное возникшее затруднение - потребовалось определение passenger_ruby, путь оказался несколько иным, чем в описании. В связи с этим после первой сборки passenger не запустился. Возможно это связано с тем, что был взят образ с определенным tag и процедура установки ruby через rvm была выключена из шагов настройки. В любом случае путь можно получить из контейнера командой:

# Запускаем bash в созданном контейнере.
% dke -t -i portfolio-webapp-1 bash -l
# Получаем путь до интерпретатора.
# passenger-config about ruby-command
passenger-config was invoked through the following Ruby interpreter:
  Command: /usr/local/rvm/gems/ruby-3.1.2/wrappers/ruby

Осталось внести правильный путь в конфигурацию и пересоздать контейнер.

% docker build -f Dockerfile.ruby -t as/portfolio .
  • -f Dockerfile.ruby - указываем файл для сборки, если он имеет иное название, чем Dockerfile

  • -t tag - даем нашему образу название.

  • точка в конце указывает каталог, где находится Dockerfile.

Запускаем созданный образ

% dkr --rm  -p 32000:80 -d as/portfolio

Если все было сделано верно, то по адресу http://:32000 находится стартовая страница приложения.

Ну если быть точным, то не стартовая страница, а сообщение Rails о том, что база данных не найдена и предложение ее создать.

Попытка создания базы данных приведет к следующей ошибке: Соединение с базой данных установить не удалось, host не найден.

Сейчас у нас должно быть два контейнера, к каждому из которых есть доступ с локального компьютера по установленным портам, но между собой они никак не связаны.

% dkIls
REPOSITORY                 TAG       IMAGE ID       CREATED         SIZE
as/portfolio               latest    727e40730bf9   20 hours ago    991MB
as/db-primary              latest    5d99017051a7   24 hours ago    382MB

Подключение контейнеров к общей сети

Процедура подключения контейнеров к общей сети описана вот здесь Networking with standalone containers

Из описания следует, что контейнеры должны были автоматически подключиться, но у меня этого не произошло. Диагностировать проблему оказалось весьма затруднительно, причина в том, что инструментарий для сетевой диагностики, включенный в выбранные образы весьма ограничен.

Если для passenger образа есть хотя бы базовые команды работы с IP стеком (ip address и прочее), то в образе для postgres этого нет вообще. Нет и иных привычных утилит, например ping и т.д.

В общем, это совершенно верный подход, в работающих настроенных контейнерах ничего лишнего быть не должно. Устанавливать все необходимое выходило за рамки задачи, поэтому выбран вариант создания своей сети для двух контейнеров и запуск их с указанием этой сети.

Как делать сети - описано в том же руководстве, кроме того еще и вот здесь habr Полное практическое руководство по Docker

% docker network create dbnet
% docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
7b7541bcbdd4   bridge              bridge    local
03af282a5313   dbnet               bridge    local
6c8c86ea1a80   host                host      local
2fd97dbf940f   none                null      local

Еще один момент - в конфигурации приложения Rails необходимо указать внутренний порт для доступа к базе, 5432, после чего еще раз пересоздать контейнер.

Запускаем наши контейнеры, указывая в качестве параметра запуска созданную нами сеть

% dkr --rm -p 54320:5432 --name db-primary --net dbnet -e POSTGRES_PASSWORD=password -d as/db-primary
% dkr --rm --net dbnet -p 32000:80 --name webapp -d as/portfolio

Смотрим состояние запущенных контейнеров.

dkls
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS                            NAMES
0f51b5e27f2f   as/portfolio    "/sbin/my_init"          8 minutes ago    Up 8 minutes    443/tcp, 0.0.0.0:32000->80/tcp   webapp
a05d34329533   as/db-primary   "docker-entrypoint.s…"   20 minutes ago   Up 20 minutes   0.0.0.0:54320->5432/tcp          db-primary

Проверяем визуальную работу приложения на порту :32000 - приложение должно сообщить о необходимости создания базы данных, потом - выполнения миграций и .. Запуститься.

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

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

Автоматизация создания контейнеров для приложения Rails

Инструмент для этого - docker-compose Docker Compose

Описание весьма внушительное и содержательное, требующее внимательного изучения, поэтому переходим сразу к разделу Try Docker Compose с надеждой на то, что все окажется не так и страшно.

Опираясь на этот tutorial и используя Полное практическое руководство по Docker создаем файл docker-compose.yml


version: '1'
services:
    db:
        hostname: db
        build:
            context: .
            dockerfile: Dockerfile.postgres
        environment:
            - PGUSER=postgres
            - POSTGRES_USER=postgres
            - POSTGRES_PASSWORD=password
        restart: always
        ports:
            - 54320:5432
    webapp:
        hostname: webapp
        build: .
        ports:
            - 32000:80
            - 22222:22
        restart: always
        depends_on:
            - db

В руководстве достаточно подробно описано, что есть что в этом файле, поэтому кратко о содержании.

Создаем два сервиса (это и есть наши будущие контейнеры)

  • db и webapp - соответственно база данных и приложение.

  • hostname - указываем "человеческое" имя хоста внутри контейнера, иначе docker подберет цифровое случайное обозначение. Не обязательно, но для внешнего администрирования контейнера и просто логов - очень полезно.

  • build - указание наших ранее созданных Dockerfile для сборки образов для контейнеров. Здесь немного поменял названия, теперь файл для сборки postgres называется Dockerfile.postgres, а для приложения - просто Dockerfile. Для стандартного Dockerfile имя указывать не обязательно.

  • ports - определяем внешний проброс портов. Для приложения добавлена возможность подключения по ssh, которая была определена ранее.

  • restart - поведение при ошибках и сбоях

  • environment - переменные окружения, все, что раньше указывали в командной строке с ключом -e переносим сюда.

  • depends_on - указываем зависимость второго контейнера от первого, с базой данных.

Удаляем контейнеры, созданные ранее, освобождаем место и запускаем процесс создания с помощью docker-compose c ключами создания и запуска с последующим detach.

% docker-compose up --build -d

И .. Это все.

Приложение вместе с базой данных помещается в два контейнера и они запускаются. Все работает.
Не надо создавать сети, все создается автоматически, контейнеры находятся в одной сети и могут взаимодействовать.

Можно просматривать логи работы контейнеров

% docker compose logs -f
--------
...
portfolio-db-1      | Готово. Теперь вы можете запустить сервер баз данных:
portfolio-db-1      |
portfolio-db-1      |     pg_ctl -D /var/lib/postgresql/data -l файл_журнала start
portfolio-webapp-1  | [ N 2023-04-02 14:24:41.0820 38/T1 age/Cor/CoreMain.cpp:1325 ]: Passenger core shutdown finished
portfolio-webapp-1  | [ N 2023-04-02 15:28:20.4107 34/T1 age/Wat/WatchdogMain.cpp:1373 ]: Starting Passenger watchdog...
portfolio-webapp-1  | [ N 2023-04-02 15:28:20.4492 37/T1 age/Cor/CoreMain.cpp:1340 ]: Starting Passenger core...
portfolio-webapp-1  | [ N 2023-04-02 15:28:20.4493 37/T1 age/Cor/CoreMain.cpp:256 ]: Passenger core running in multi-application mode.
portfolio-webapp-1  | [ N 2023-04-02 15:28:20.4587 37/T1 age/Cor/CoreMain.cpp:1015 ]: Passenger core online, PID 37
portfolio-webapp-1  | [ N 2023-04-02 15:28:22.7382 37/T5 age/Cor/SecurityUpdateChecker.h:519 ]: Security update check: no update found (next check in 24 hours)
portfolio-db-1      |
portfolio-db-1      | initdb: предупреждение: включение метода аутентификации "trust" для локальных подключений
portfolio-db-1      | initdb: подсказка: Другой метод можно выбрать, отредактировав pg_hba.conf или ещё раз запустив initdb с ключом -A, --auth-local или --auth-host.
...
--------

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

Первая сборка проходит довольно долго, в основном это связано с загрузкой начальных образов и gem пакетов для приложения, зато повторный запуск осуществляется очень быстро.
Если сравнивать со временем запуска подобной конструкции в виртуальной машине - отличаются порядки.

Следующий интересный шаг, который можно было бы попробовать - разместить приложение в cloud, чтобы протестировать, насколько это переносимо, но aws отключил регистрацию пользователей из России и обходить это нет никакого желания.

Краткие итоги.

  • Технология вызывает сильное уважение и восхищение - количество "не понятных" и сложных моментов - минимально.

  • Повторное использование уже созданных контейнеров путем добавления/изменения конфигураций сборки предоставляет большие возможности.

  • Возможности организации совместной работы контейнеров с использованием подключаемых томов

  • Можно использовать предварительные image для последующего построения контейнеров.

  • Клонирование контейнеров и последующее использование для масштабирования

  • И еще достаточно много полезных и очень полезных возможностей. :-)

И если добавить к этому возможность управления всем этим процессом с учетом балансировок нагрузки, предоставляемую Kubernetes - многие процессы, организация кластера для базы данных, балансировка нагрузки приложения, обеспечение отказоустойчивости и т.д. и т.п. становятся существенно проще.

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


  1. SibProgrammer
    02.04.2023 16:38
    +4

    Если вас интересует эта тема, то есть смысл посмотреть mrsk - https://github.com/mrsked/mrsk Пилит его сам DHH (автор Rails).


  1. aelaa
    02.04.2023 16:38

    dkpl

    Используете алиасы в статье - хоть расшифровывайте.

    А вообще расписать на 5 экранов 2 файла - шедевр конечно.


    1. Ale2Da Автор
      02.04.2023 16:38

      dk=docker
      dkr='docker run'
      dkIb='docker image build'
      dke='docker exec'
      dkIls='docker image ls'
      dkpl='docker pull'


  1. bugagazavr
    02.04.2023 16:38

    В первую очередь статья выглядит переусложненной, если верить вашему Dockerfile, то это сетап для локальной разработки.

    Например непонятно зачем было брать контейнер с passenger и что мешало взять образ ruby:3.1.2 и для тестового запуска использовать puma, ну или passenger в standalone режиме. А так получилось нагромождение, например nginx, который для локальной rails-разработки не нужен.

    А еще вы прокидываете SSH ключ в контейнер и экспоузите SSH порт, зачем?! Что бы попасть в контейнер есть команда docker exec. Более того запуск более одного процесса в рамках docker контейнера - это плохая практика, да для локальной разработки "итак сойдет", но лучше не учить людей плохому.

    Создается впечатление, что описанный вами опыт в статье плохо систематизирован.


    1. Ale2Da Автор
      02.04.2023 16:38

      А это и есть "опыт". Только не использования, а что получается, если делать "первый раз".
      По существу.
      1. Это не для разработки. Разрабатывать в такой конструкции я бы не стал. Песочница для использования дальше, можно так назвать. Основные приемы. Где взять, куда посмотреть, как собрать все в кучу.
      2. SSH в этой конфигурации - не нужен вообще, все на локальной машине.
      Если не на локальной - может понадобиться, потому что не всегда там, где контейнер разворачивается, есть доступ к какой либо командной строке.
      3. Nginx и passenger выбраны по простой причине - это наиболее просто превратить в prod решение.
      4. Возможно забыл отметить, здесь вообще нет никаких затронутых вопросов промышленной эксплуатации. Так, песочница, не более того.