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.
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é.
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
endExécutez ensuite bundle install pour installer les gems dans les versions indiquées.
Configurer Capistrano
cap installCette 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.
# 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.
# 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 certbotConfigurez votre environnement de production dans config/deploy/production.rb 👇🏾
# 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 :
-
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.
-
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, truedans config/deploy.rb. Sinon, vous devrez créer manuellement le service du socket.
config/deploy/templates/puma.rb.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 👇🏾
[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 👇🏾
[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.targetEnvoyer 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é.
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 servicePrise en charge du fichier .env
Modifiez config/application.rb pour prendre en charge les fichiers .env.
# ...
# Load .env file
Dotenv::Rails.loadPrise 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.
# remote server
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx
sudo service nginx reloadDéployer l’application
Maintenant que tout est prêt, je peux lancer le déploiement avec la commande suivante :
cap production deployCette commande effectue plusieurs actions pour déployer une version complète et fonctionnelle de votre application :
-
Le dépôt Git est cloné dans un nouveau dossier sous
releases/{timestamp}sur votre serveur distant. -
Liens symboliques vers le dossier partagé : les fichiers et dossiers définis dans
linked_filesetlinked_dirsdans votre configuration Capistrano sont liés symboliquement du dossiersharedvers le dossiercurrent. -
Installation des dépendances : les dépendances Ruby définies dans votre
Gemfilesont installées avecbundle install. -
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.
-
Exécution des migrations : les migrations nécessaires sont lancées avec
bundle exec rails db:migrateafin de mettre à jour la structure de la base de données. -
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 !