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)
Разобьем задачу на этапы:
Установка Docker
Перенос базы PostgreSQL в контейнер.
Подключение контейнера к работающему приложению.
Перенос приложения в контейнер
Подключение приложения из контейнера к контейнеру с базой данных.
Использование возможностей Docker для автоматизации данного процесса.
Посмотрим, что получается и что можно сделать дальше.
Установка 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
1.29.1-ruby3.1 - 343.89 MB
phusion/passenger-ruby31:2.3.0 - 255.28 MB
По размерам примерно одинаковые, возьмем версию с 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)
bugagazavr
02.04.2023 16:38В первую очередь статья выглядит переусложненной, если верить вашему Dockerfile, то это сетап для локальной разработки.
Например непонятно зачем было брать контейнер с passenger и что мешало взять образ ruby:3.1.2 и для тестового запуска использовать puma, ну или passenger в standalone режиме. А так получилось нагромождение, например nginx, который для локальной rails-разработки не нужен.
А еще вы прокидываете SSH ключ в контейнер и экспоузите SSH порт, зачем?! Что бы попасть в контейнер есть команда docker exec. Более того запуск более одного процесса в рамках docker контейнера - это плохая практика, да для локальной разработки "итак сойдет", но лучше не учить людей плохому.
Создается впечатление, что описанный вами опыт в статье плохо систематизирован.
Ale2Da Автор
02.04.2023 16:38А это и есть "опыт". Только не использования, а что получается, если делать "первый раз".
По существу.
1. Это не для разработки. Разрабатывать в такой конструкции я бы не стал. Песочница для использования дальше, можно так назвать. Основные приемы. Где взять, куда посмотреть, как собрать все в кучу.
2. SSH в этой конфигурации - не нужен вообще, все на локальной машине.
Если не на локальной - может понадобиться, потому что не всегда там, где контейнер разворачивается, есть доступ к какой либо командной строке.
3. Nginx и passenger выбраны по простой причине - это наиболее просто превратить в prod решение.
4. Возможно забыл отметить, здесь вообще нет никаких затронутых вопросов промышленной эксплуатации. Так, песочница, не более того.
SibProgrammer
Если вас интересует эта тема, то есть смысл посмотреть mrsk - https://github.com/mrsked/mrsk Пилит его сам DHH (автор Rails).