Есть много причин, по которым мы хотели бы докеризировать наше приложение Rails.В моем случае главная причина в том, что я хочу быть независимым от хостинга.Я хочу легко перенести свое приложение с одного хостинга на другой с наименьшим возможным трением.
Я годами мучаюсь, платя очень дорогой выделенный сервер только из-за того, что было слишком сложно снова все настроить вдешевле.Это может быть нормально, если у вас есть одно приложение для миграции, но у меня было ~ 20 приложений, запущенных на этом сервере, каждое со своими особенностями.Некоторые из них требуют cronjobs, другие требуют специальных внешних инструментов, таких как специальная версия FFmpeg
, базы данных, ...
Если бы каждое из этих приложений имело свою собственную настройку Dockercompose, перемещение их на другой сервер было бы намного меньшеболезненный.
Итак, я начал Dockerize (и Dockercompose) все мои приложения, и вот чему я научился.
Это пример фиксации одного из Dockerize одного из моих старых приложений:
Вот что мы собираемся построить:
Настройка всехDocker
В этом учебном пособии есть две части, одна из которых посвящена самой докеризации, а вторая будет посвящена настройке сервера и запуску приложения.
Службы
Давайте предположим, что вашему приложению понадобятся все эти службы:
- MySql
- Cronjobs
- Nginx (каквнешний интерфейс прокси), с SSL
- Приложение (само приложение Rails)
Итак, это 4 Сервиса, и мы собираемся создать контейнер Docker для каждого из них, и мы собираемся, чтобы пользователь Dockercompose обернул конфигурацию и собрал всеиз них
Мы создаем нашу конфигурацию Dockercompose в корневой папке нашего приложения:
# ./docker-compose.yml
version: '3'
services:
db:
image: mysql:5.7
volumes:
- ./db/data:/var/lib/mysql
restart: always
ports:
- 127.0.0.1:3306:3306
environment:
MYSQL_ROOT_PASSWORD: root
app:
build:
context: .
dockerfile: ./docker/app/Dockerfile
volumes:
- .:/var/www/app
restart: always
depends_on:
- db
web:
build:
context: .
dockerfile: ./docker/web/Dockerfile
depends_on:
- app
ports:
- 80:80
- 443:443
volumes:
- .:/var/www/app
restart: always
cron:
build:
context: .
dockerfile: ./docker/cron/Dockerfile
volumes:
- .:/var/www/app
restart: always
depends_on:
- db
Давайте рассмотрим сервис по сервисам.
Сервис MySQL
db:
image: mysql:5.7
volumes:
- ./db/data:/var/lib/mysql
restart: always
ports:
- 127.0.0.1:3306:3306
environment:
MYSQL_ROOT_PASSWORD: root
Некоторые комментарии:
image: mysql:5.7
Это наиболее простой в настройке сервис, поскольку он является стандартным и мы используем общедоступный образ mysql:5.7
.Не забудьте добавить версию, чтобы она не изменилась в будущем, пока вас не заметят.
volumes:
- ./db/data:/var/lib/mysql
Одна важная вещь, которую мы настраиваем здесь, находится в разделе volumes
.Мы связываем одну внутреннюю папку контейнера с внешней папкой.Папка, о которой идет речь, - /var/lib/mysql
, что, что неудивительно, - здесь хранятся все данные БД.Мы не хотим, чтобы эти данные сохранялись во внутренней папке контейнера, потому что в этом случае они не будут постоянными при перезапуске контейнера.Поэтому мы связали его с внешней папкой, в данном случае: APP_ROOT/db/data
.
Одним из важных следствий является то, что двоичные данные из MySQL будут храниться в папке приложения, поэтому мы должны быть уверены, что мы неотправка его в репо:
echo "/db/data" >> .gitignore
Порты:
ports:
- 127.0.0.1:3306:3306
Другая внутренняя / внешняя конфигурация соединения - ports
.Это основной.Внутренняя служба mysql будет прослушивать порт 3306
по умолчанию, поэтому мы делаем его доступным извне, используя эту конфигурацию
environment:
MYSQL_ROOT_PASSWORD: root
Это изображение требует настройки ENVVAR для настройки пользователя root
в основной базе данных MySQL.Тот факт, что мы включим его здесь, может быть проблемой безопасности, но я не собираюсь включать решение для этой проблемы в это Tuto.
Теперь нам нужно настроить наш database.yml
для использования образа док-станции MySQL в качествеa host
# /config/database.yml
production:
<<: *default
host: db
database: myapp
password: root
См., что конфигурация host
указывает на db
, который является хостом, созданным Dockercompose.
Служба приложений Rails
Thisэто определение контейнера, который станет домом для нашего приложения Rails.
app:
build:
context: .
dockerfile: ./docker/app/Dockerfile
volumes:
- .:/var/www/app
restart: always
depends_on:
- db
Некоторые комментарии:
dockerfile: ./docker/app/Dockerfile
Именно здесь мы сказали Dockercompose, где найти сборку.конфигурация для этого контейнера Docker.
volumes:
- .:/var/www/app
Некоторые ссылки здесь.Мы связываем внутреннюю папку контейнера /var/www/app
с корнем нашего приложения.
Служба прокси Nginx
Вместо того, чтобы подвергать наше Rails-приложение напрямую HTTP-запросам, я думаю, что хорошей идеей будет поставить мощный прокси-сервер.Это добавит некоторые функции обработки пула запросов, поддержки https и доставки статических файлов.
web:
build:
context: .
dockerfile: ./docker/web/Dockerfile
depends_on:
- app
ports:
- 80:80
- 443:443
volumes:
- .:/var/www/app
restart: always
Нам были переданы важные части этого файла конфигурации в предыдущих разделах.
ports:
- 80:80
- 443:443
Вк этой услуге мы подключаем 2 разных порта.Один для подключений http, а другой для подключений https.
Служба задач Cron
Я предполагаю, что задачи cron, которые мы собираемся настроить, являются зависимостью приложения Rails.Это будут rake
вызовы или curl
запросы к некоторым из наших конечных точек приложения Rails.
Это потому, что я также связываю домен APP_ROOT с внутренней папкой.
cron:
build:
context: .
dockerfile: ./docker/cron/Dockerfile
volumes:
- .:/var/www/app
restart: always
depends_on:
- db
Контейнеры
Каждый сервис будет предоставляться для отдельного контейнера Docker.
Для контейнера earch нам нужен файл конфигурации.Для самоорганизации я размещаю всю конфигурацию контейнера Docker в папке:
./docker
Контейнер MySQL
Для него не требуется файл Docker, поскольку мы используем общедоступный образ.
Контейнер приложения Rails
# ./docker/app/Dockerfile
FROM ruby:2.5.1
# Install dependencies
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
# Set an environment variable where the Rails app is installed to inside of Docker image:
ENV RAILS_ROOT /var/www/app
RUN mkdir -p $RAILS_ROOT
# Set working directory, where the commands will be ran:
WORKDIR $RAILS_ROOT
# Setting env up
ENV RAILS_ENV="production"
ENV RACK_ENV="production"
ENV SECRET_KEY_BASE="8704xxhhb0b5889cb81d8452a218251f7940d285ffgg79a3c9b4108dd1e9875227a868bb122bc23c833432ca37b3fe7b7c514xxccc9285661e5b2ce8a5a53453"
# Adding gems
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install --jobs 20 --retry 5 --without development test
# Adding project files
COPY . .
RUN bundle exec rake assets:precompile
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Это базовый Dockerfile приложения Rails.С помощью короткого сеанса Google вы можете найти объяснение всех вещей, которые здесь происходят.
Может быть другая проблема безопасности, добавив сюда SECRET_KEY_BASE
, но, как я уже сказал, я не буду усложнять это руководство проблемами безопасности.Я держу его там в качестве примера того, как вы можете настроить пользовательские ENVVAR для ваших контейнеров Docker.
Контейнер Nginx Proxy
В этом контейнере мы включаем 2 разных файла конфигурации, один из которыхDockerfile сам по себе, а другой - настраиваемая конфигурация для ngnix.
# ./docker/web/Dockerfile
# Base image:
FROM nginx
# Install dependencies
RUN apt-get update -qq && apt-get -y install apache2-utils
# establish where Nginx should look for files
ENV RAILS_ROOT /var/www/app
# Set our working directory inside the image
WORKDIR $RAILS_ROOT
# create log directory
RUN mkdir log
# copy over static assets
COPY public public/
# Copy Nginx config template
COPY docker/web/nginx.conf /tmp/docker.nginx
# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$RAILS_ROOT' < /tmp/docker.nginx > /etc/nginx/conf.d/default.conf
EXPOSE 80
# Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`)
CMD [ "nginx", "-g", "daemon off;" ]
Это настроит сервер nginx, наиболее интересная часть здесь может быть такая:
# Copy Nginx config template
COPY docker/web/nginx.conf /tmp/docker.nginx
# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$RAILS_ROOT' < /tmp/docker.nginx > /etc/nginx/conf.d/default.conf
Где мы копируемнаш пользовательский файл конфигурации nginx в контейнер в качестве конфигурации по умолчанию.Мы также делаем некоторую динамическую замену, чтобы избежать записи определенных PATH в наш файл шаблона конфигурации.
И вот наш файл шаблона конфигурации nginx:
# ./docker/web/nginx.conf
# This is a template. Referenced variables (e.g. $RAILS_ROOT) need
# to be rewritten with real values in order for this file to work.
upstream rails_app {
server app:3000;
}
# Default server
server {
# define your domain
# server_name localhost;
listen 80;
listen 443 ssl;
ssl_certificate $RAILS_ROOT/secret/ssl_certificates/myapp.com.crt;
ssl_certificate_key $RAILS_ROOT/secret/ssl_certificates/myapp.com.key;
# define the public application root
root $RAILS_ROOT/public;
index index.html;
# define where Nginx should write its logs
access_log $RAILS_ROOT/log/nginx.access.log;
error_log $RAILS_ROOT/log/nginx.error.log;
# deny requests for files that should never be accessed
location ~ /\. {
deny all;
}
location ~* ^.+\.(rb|log)$ {
deny all;
}
# serve static (compiled) assets directly if they exist (for rails production)
location ~ ^/(assets|images|javascripts|stylesheets|swfs|system)/ {
try_files $uri @rails;
access_log off;
gzip_static on; # to serve pre-gzipped version
expires max;
add_header Cache-Control public;
# Some browsers still send conditional-GET requests if there's a
# Last-Modified header or an ETag header even if they haven't
# reached the expiry date sent in the Expires header.
add_header Last-Modified "";
add_header ETag "";
break;
}
# send non-static file requests to the app server
location / {
try_files $uri @rails;
}
location @rails {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://rails_app;
}
}
Конфигурация очень проста.Я думаю, что наиболее интересным моментом является то, где мы настраиваем наши сертификаты SSL:
ssl_certificate $RAILS_ROOT/secret/ssl_certificates/myapp.com.crt;
ssl_certificate_key $RAILS_ROOT/secret/ssl_certificates/myapp.com.key;
Эта конфигурация ожидает, что у нас будут сертификаты в этой папке.Я рекомендую не добавлять эти файлы в репозиторий и добавлять их вручную на своем сервере после развертывания приложения.
Контейнер задач Cron
Как я уже говорил ранее, я предполагаю, чтонаши задачи cron будут иметь наше приложение Rails как зависимость, поэтому этот контейнер имеет ту же конфигурацию, что и наш контейнер Rail App, и некоторые дополнительные вещи:
# ./docker/cron/Dockerfile
FROM ruby:2.5.1
# Install dependencies
RUN apt-get update && apt-get -y install cron
# Set an environment variable where the Rails app is installed to inside of Docker image:
ENV RAILS_ROOT /var/www/app
RUN mkdir -p $RAILS_ROOT
# Set our working directory inside the image
WORKDIR $RAILS_ROOT
# Setting env up
ENV RAILS_ENV="production"
ENV RACK_ENV="production"
ENV SECRET_KEY_BASE="87042df8b0b5889cb81d8452a218251f7940d2854fde79a3c9b4108dd1e9875227a868bb122bc23c833432ca37b3fe7b7c51453a0c9285661e5b2ce8a5a53453"
# Adding gems
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install --jobs 20 --retry 5 --without development test
# Adding project files
COPY . .
RUN bundle exec rake assets:precompile
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
## Cron config
# Add crontab file to the cron.d directory
COPY crontab /etc/cron.d/app
# Give execution rights on the cron job
# Files in /etc/cron.d can not have names with "-" or ".". It can be problematic
RUN chmod 0644 /etc/cron.d/app
# To load the env variables in cron sessions
# without this the user in the cron session won't be able to find commands and Gems
RUN printenv | grep -v "no_proxy" >> /etc/environment
# Run the command on container startup
CMD ["cron", "-f"]
Итак, единственными новинками являются:
## Cron config
# Add crontab file to the cron.d directory
COPY crontab /etc/cron.d/app
# Give execution rights on the cron job
# Files in /etc/cron.d can not have names with "-" or ".". It can be problematic
RUN chmod 0644 /etc/cron.d/app
# To load the env variables in cron sessions
# without this the user in the cron session won't be able to find commands and Gems
RUN printenv | grep -v "no_proxy" >> /etc/environment
# Run the command on container startup
CMD ["cron", "-f"]
И самая важная часть из вышеперечисленного - это то, где мы устанавливаем внутреннюю конфигурацию crontab:
COPY crontab /etc/cron.d/app
Ожидается найти файл в вашем APP_ROOT с совместимой конфигурацией crontab, например:
# ./crontab
0 * * * * root /bin/bash -l -c 'cd $RAILS_ROOT && bundle exec rake myapp:mytask'
Сервер, настройка и развертывание
Теперь, когда наше Rails-приложение полностью загружено, мы хотим его развернуть.
Я не собираюсь использовать необычное развертываниеинструменты, в моем случае я совершенно счастлив, потянув репо моего приложения вручную на моем сервере.Конечно, это не будет масштабироваться в профессиональной среде с несколькими развертываниями в день.Но для простоты я не собираюсь рассказывать об автоматических развертываниях в этом руководстве.
Установка зависимостей
Даже если у вас настроен самый причудливый докер, нам все равно придется много работать на нашем сервере, чтобыподготовить его поддержать наши вещи.
Это включает в себя:
- git (базовый, если мы хотим использовать в качестве механизма передачи файлов)
- Docker (сюрприз!)
- Dockercompose
Настройка кластера Dockercompose
После того, как мы установили все зависимости, нам нужно:
Загрузите наш код приложения
git clone https://github.com/fguillen/MyApp.git
Создание наших изображений
docker-compose build
Настройка всех контейнеров / услуг
docker-compose up -d
Проверьте, все ли прошло хорошо:
docker-compose logs
Выполнение базовых предварительных задач Rails
docker-compose exec app bundle exec rake db:create db:schema:load
docker-compose exec app bundle exec rake db:seed # Optional
Сценарий настройки
Для выполнения всех вышеперечисленных задач я создал этот файл, который
может или не может работать для вас из коробки, но определенно будет ориентировать вас
в правильном направлении.
У меня это работает в дистрибутивах Ubuntu.
# ./server_setup.sh
#!/bin/bash
set -e
set -x
apt-get update
apt-get install git-core
# Install Docker
# From here: https://docs.docker.com/install/linux/docker-ce/ubuntu/#set-up-the-repository
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
apt-get install docker-ce
# Docker compose
# From here: https://docs.docker.com/compose/install/
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
# Download the App
mkdir -p /var/apps
cd /var/apps
git clone https://user@github.com/user/myrepo.git
# Start the App
cd /var/apps/MyApp
docker-compose build
docker-compose up -d
docker-compose exec app bundle exec rake db:create db:schema:load
# docker-compose exec app bundle exec rake db:seed # Optional
Я использую для включения этого файла в репозиторий приложений по пути:
./docker/server_setup.sh
Выводы
Докеризация приложения Rails совсем не тривиальна. Это может занять много времени.
Много пробных ошибок. Многие вещи терпят неудачу, и вы не знаете почему.
Я надеюсь, что это руководство хотя бы немного уменьшит всю эту боль.
Как только это сработает, шансы, которые сработают в следующий раз, велики;)