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 - многие процессы, организация кластера для базы данных, балансировка нагрузки приложения, обеспечение отказоустойчивости и т.д. и т.п. становятся существенно проще.
