Déployer Ruby on Rails avec Capistrano, Puma et Nginx simplement

Illustration de Déployer Ruby on Rails avec Capistrano, Puma et Nginx simplement

Un guide pratique pour déployer Rails avec Capistrano, Puma et Nginx sur un VPS Ubuntu.

J’ai récemment terminé le développement de ma première application Ruby on Rails 7.1. Une fois le développement achevé vient l’étape cruciale du déploiement. Dans mon cas, j’ai choisi un VPS Ubuntu hébergé sur AWS LightSail. Après plusieurs heures de recherche et de débogage, j’ai décidé de partager ici une synthèse de ce que j’ai appris.

Contexte

J’ai développé une application qui communique avec une API dont les URL sont définies dans un fichier .env. Pour la base de données, j’ai choisi SQLite. Oui, SQLite peut être utilisé en production. Il suffit d’ajouter config.active_record.sqlite3_production_warning = false dans config/environments/production.rb pour supprimer les avertissements.

Il existe de nombreuses méthodes et de nombreux outils pour déployer une application. Dans cet article, j’utiliserai Capistrano, Nginx et Puma.

  • Capistrano : un outil d’automatisation et de déploiement sur des serveurs distants, écrit en Ruby.

  • Puma : un serveur web rapide et concurrent pour Ruby et Rack.

  • Nginx : un serveur web qui agit comme proxy inverse et redirige les requêtes vers l’application Ruby on Rails.

Capistrano me permettra d’automatiser l’ensemble du processus de déploiement. Un accès SSH au serveur cible est indispensable. Le code à déployer doit également être versionné, par exemple avec Git.

Passons à la pratique

Préparer le serveur distant

Commencez par vérifier que la version de Ruby installée sur votre serveur est identique à celle de votre environnement de développement. Pour la gérer simplement, vous pouvez utiliser RVM ou rbenv. Consultez la documentation de l’outil choisi. Une fois Ruby installé, installez également Bundler afin de pouvoir installer les différentes dépendances de l’application.

Bash
gem install bundler

J’utilise yarn et webpack pour compiler les ressources. Si c’est aussi votre cas, installez Node.js, ainsi que tout autre outil indispensable au bon fonctionnement de votre application (Redis, MySQL, etc.).

Installer les gems

Maintenant que tout est en place, je dois installer les gems suivantes. Les contraintes de version sont importantes pour éviter les problèmes de compatibilité.

Ruby
gem 'puma', '~> 6.0.0', '< 7'
gem 'dotenv-rails' # to handle .env files
gem 'sd_notify', '~> 0.1.0' # hotfix not actually used

group :development do
  gem 'capistrano'
  gem 'capistrano3-puma', '6.0.0.beta.1' # supports puma 6+
  gem 'capistrano-rails'
  gem 'capistrano-rvm' # capistrano-rbenv for rbenv
end

Exécutez ensuite bundle install pour installer les gems dans les versions indiquées.

Configurer Capistrano

Bash
cap install

Cette commande crée plusieurs fichiers et dossiers dans votre projet :

  • Capfile

  • config/deploy.rb

  • config/deploy/production.rb

  • config/deploy/staging.rb

Modifions le Capfile pour inclure les extensions nécessaires.

Ruby
# frozen_string_literal: true

require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git

# Include tasks from other gems included in your Gemfile
require 'capistrano/rvm' # 'capistrano/rbenv' for rbenv
require 'capistrano/bundler'
require 'capistrano/rails' # will include assets and migrations tasks
require 'capistrano/puma'
require 'capistrano/puma/nginx'
install_plugin Capistrano::Puma
install_plugin Capistrano::Puma::Systemd

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

Pour personnaliser le déploiement, je dois modifier le fichier config/deploy.rb.

Ruby
# frozen_string_literal: true
lock '~> 3.19.1'

set :application, 'ecovet.cloud' # your app name
set :repo_url, 'git@github.com:bernard-ng/ecovet.cloud.git'
set :branch, 'main'
set :deploy_to, '/var/www/html/ecovet.cloud' # path on remote server

set :pty, true
append :linked_files, 'config/master.key', '.env', 'config/database.yaml'
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor', 'storage'

set :keep_releases, 2
set :ssh_options, {
  forward_agent: true,
  auth_methods: %w[publickey],
  keys: %w[~/.ssh/LightsailDefaultKey-eu-west-2.pem]
}

# puma
set :puma_workers, 2 # check your CPU specs
set :puma_rackup, -> { File.join(current_path, 'config.ru') }
set :puma_state, "#{shared_path}/tmp/pids/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"
set :puma_default_control_app, "unix://#{shared_path}/tmp/sockets/pumactl.sock"
set :puma_access_log, "#{shared_path}/log/puma_access.log"
set :puma_error_log, "#{shared_path}/log/puma_error.log"
set :puma_conf, "#{shared_path}/puma.rb"

set :puma_control_app, false
set :puma_systemctl_user, :system
set :puma_service_unit_type, 'simple' # or notify
set :puma_enable_socket_service, true # mendatory in our case

# nginx
set :nginx_config_name, 'ecovet.cloud'
set :nginx_server_name, 'ecovet.cloud'
set :nginx_use_ssl, false # will be handled by certbot

Configurez votre environnement de production dans config/deploy/production.rb 👇🏾

Ruby
# frozen_string_literal: true
# config/deploy/production.rb

server 'ecovet.cloud', user: 'ubuntu', roles: %w[app db web], ssh_options: { forward_agent: true }

Modèles de configuration

Puma, mon serveur web, doit rester disponible en permanence. Même si la machine redémarre ou rencontre une panne, Puma doit se relancer automatiquement. Pour cela, je peux utiliser systemd, un gestionnaire de services qui démarre, arrête et supervise automatiquement les processus.

La configuration de Puma avec systemd se divise en deux parties :

  1. La première concerne la définition du service Puma : son mode de démarrage, l’utilisateur qui l’exécute et sa stratégie de redémarrage.

  2. La seconde concerne la configuration du socket Puma, qui gère la communication entre Puma et Nginx, ou tout autre composant utilisant ce socket pour transmettre des requêtes HTTP.

Voici les modèles de configuration à ajouter à votre projet et à adapter si nécessaire. Au moment de la rédaction, la gem capistrano3-puma n’avait pas été mise à jour depuis deux ans, ce qui pouvait entraîner des problèmes avec sa configuration par défaut.

  • config/deploy/templates/puma.rb.erb

  • config/deploy/templates/puma.service.erb

  • config/deploy/templates/puma.socket.erb, vous devez définir set :puma_enable_socket_service, true dans config/deploy.rb. Sinon, vous devrez créer manuellement le service du socket.

config/deploy/templates/puma.rb.erb 👇🏾

ERB
#!/usr/bin/env puma

directory '<%= current_path %>'
rackup "<%=fetch(:puma_rackup)%>"
environment '<%= fetch(:puma_env) %>'
<% if fetch(:puma_tag) %>
  tag '<%= fetch(:puma_tag)%>'
<% end %>
pidfile "<%=fetch(:puma_pid)%>"
state_path "<%=fetch(:puma_state)%>"
stdout_redirect '<%=fetch(:puma_access_log)%>', '<%=fetch(:puma_error_log)%>', true


threads <%=fetch(:puma_threads).join(',')%>

<%= puma_bind %>
<% if fetch(:puma_control_app) %>
activate_control_app "<%= fetch(:puma_default_control_app) %>"
<% end %>
workers <%= puma_workers %>
<% if fetch(:puma_worker_timeout) %>
worker_timeout <%= fetch(:puma_worker_timeout).to_i %>
<% end %>

<% if puma_preload_app? %>
preload_app!
<% else %>
prune_bundler
<% end %>

on_restart do
  puts 'Refreshing Gemfile'
  ENV["BUNDLE_GEMFILE"] = "<%= fetch(:bundle_gemfile, "#{current_path}/Gemfile") %>"
end

<% if puma_preload_app? and fetch(:puma_init_active_record) %>
on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.establish_connection
  end
end
<% end %>

config/deploy/templates/puma.service.erb 👇🏾

ERB
[Unit]
Description=Puma HTTP Server for <%= "#{fetch(:application)} (#{fetch(:stage)})" %>
<%= "Requires=#{fetch(:puma_service_unit_name)}.socket" if fetch(:puma_enable_socket_service) %>
After=syslog.target network.target

[Service]
Type=<%= service_unit_type %>
WatchdogSec=10
<%="User=#{puma_user(@role)}" if fetch(:puma_systemctl_user) == :system %>
WorkingDirectory=<%= current_path %>
ExecStart=<%= expanded_bundle_command %> exec --keep-file-descriptors puma -e <%= fetch(:puma_env) %> -C /var/www/html/ecovet.cloud/shared/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
PIDFile=<%= fetch(:puma_pid)%>
<%- Array(fetch(:puma_service_unit_env_files)).each do |file| %>
<%="EnvironmentFile=#{file}" -%>
<% end -%>
<% Array(fetch(:puma_service_unit_env_vars)).each do |environment_variable| %>
<%="Environment=\"#{environment_variable}\"" -%>
<% end -%>

# if we crash, restart
RestartSec=1
Restart=on-failure

<%="StandardOutput=append:#{fetch(:puma_access_log)}" if fetch(:puma_access_log) %>
<%="StandardError=append:#{fetch(:puma_error_log)}" if fetch(:puma_error_log) %>

SyslogIdentifier=<%= fetch(:puma_service_unit_name) %>
[Install]
WantedBy=<%=(fetch(:puma_systemctl_user) == :system) ? "multi-user.target" : "default.target"%>

config/deploy/templates/puma.socket.erb 👇🏾

ERB
[Unit]
Description=Puma HTTP Server Accept Sockets for <%= "#{fetch(:application)} (#{fetch(:stage)})" %>

[Socket]
<% puma_binds.each do |bind| -%>
<%= "ListenStream=#{bind.local.address}" %>
<% end -%>

Accept=no
<%= "NoDelay=true" if fetch(:puma_systemctl_user) == :system %>
ReusePort=true
Backlog=1024

SyslogIdentifier=puma_socket

[Install]
WantedBy=sockets.target

Envoyer les configurations de Nginx et Puma

Après avoir ajouté ces configurations, vous pouvez les envoyer vers votre serveur distant avec les commandes Capistrano suivantes. Cette étape n’est pas nécessaire si vos configurations Puma, Nginx ou systemd n’ont pas changé.

Bash
cap production puma:config # will upload puma.rb
cap production puma:nginx_config # will upload nginx config
cap production puma:install # will create systemd service and stocket
cap production puma:start # will start the puma service

Prise en charge du fichier .env

Modifiez config/application.rb pour prendre en charge les fichiers .env.

Ruby
# ...
# Load .env file
Dotenv::Rails.load

Prise en charge de HTTPS

Let’s Encrypt est une autorité de certification qui permet d’obtenir et d’installer facilement des certificats TLS/SSL afin d’activer une connexion HTTPS chiffrée sur un serveur web. Son client, Certbot, automatise la plupart, voire la totalité, des étapes nécessaires. L’obtention et l’installation d’un certificat sont entièrement automatisées pour Apache comme pour Nginx.

Bash
# remote server
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx
sudo service nginx reload

Déployer l’application

Maintenant que tout est prêt, je peux lancer le déploiement avec la commande suivante :

Bash
cap production deploy

Cette commande effectue plusieurs actions pour déployer une version complète et fonctionnelle de votre application :

  1. Le dépôt Git est cloné dans un nouveau dossier sous releases/{timestamp} sur votre serveur distant.

  2. Liens symboliques vers le dossier partagé : les fichiers et dossiers définis dans linked_files et linked_dirs dans votre configuration Capistrano sont liés symboliquement du dossier shared vers le dossier current.

  3. Installation des dépendances : les dépendances Ruby définies dans votre Gemfile sont installées avec bundle install.

  4. Précompilation des ressources : si nécessaire, les ressources de votre application sont précompilées avec Yarn et Webpack afin de préparer l’application pour la production.

  5. Exécution des migrations : les migrations nécessaires sont lancées avec bundle exec rails db:migrate afin de mettre à jour la structure de la base de données.

  6. Démarrage ou redémarrage de Puma : Puma est démarré ou redémarré avec le fichier de configuration indiqué (config/puma.rb), ce qui rend votre application web disponible sur le serveur.

Conclusion

J’espère que cet article vous fera gagner du temps et vous aidera à mieux comprendre le processus de déploiement. Ecovet.cloud est maintenant en ligne, mais comme il s’agit d’un projet d’étude, le site ne restera pas disponible très longtemps.

Bon développement !