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

Configurer Capistrano, Puma, Nginx et systemd pour déployer une application Rails 7.1 sur un VPS Ubuntu.
J’ai déployé ma première application Ruby on Rails 7.1 sur un VPS Ubuntu hébergé chez AWS Lightsail. Il m’a fallu plusieurs heures pour réunir une configuration qui fonctionne. Voici les étapes et les fichiers que j’aurais aimé trouver au départ.
Contexte
L’application communique avec une API dont les URL viennent d’un fichier .env. Elle utilise SQLite en production. La ligne config.active_record.sqlite3_production_warning = false dans config/environments/production.rb supprime l’avertissement de Rails.
Ce déploiement repose sur Capistrano, Nginx et Puma.
-
Capistrano automatise le déploiement sur les serveurs distants.
-
Puma exécute l’application Ruby et Rack.
-
Nginx agit comme proxy inverse devant Puma.
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.
Préparer le déploiement
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
Ajoutez les extensions nécessaires dans Capfile.
# 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 }Personnalisez ensuite 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 l’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. Définissez aussiset :puma_enable_socket_service, truedansconfig/deploy.rb, sinon vous devrez créer le service du socket vous-même.
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 charger le fichier .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
Une fois la configuration terminée, lancez le déploiement avec cette commande :
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. -
Capistrano crée les liens symboliques définis dans
linked_filesetlinked_dirsdepuis le dossiersharedverscurrent. -
bundle installinstalle les dépendances Ruby duGemfile. -
Yarn et Webpack précompilent les ressources lorsque le projet l’exige.
-
bundle exec rails db:migrateapplique les migrations à la base de données. -
Capistrano démarre ou redémarre Puma avec
config/puma.rb.
Lancer le déploiement
Cette configuration a mis Ecovet.cloud en ligne. Le site était un projet d’étude et n’avait pas vocation à rester disponible durablement. Les fichiers présentés ici restent toutefois une base concrète pour un déploiement Rails avec des versions comparables de Capistrano et Puma.