Déployer une application Rails sur DigitalOcean
Suivez ça sur votre propre serveur. Commencer gratuitement, sans carte
Votre application Rails fonctionne sur votre ordinateur et vous devez maintenant la faire tourner sur un vrai serveur, avec un vrai domaine, une vraie base de données, et du HTTPS qui se renouvelle tout seul. La plupart des guides s’arrêtent à « docker build » et vous laissent trouver le reste par vous-même, ou vous donnent une plateforme hébergée avec un formulaire de carte de crédit. Celui-ci vous amène d’un compte DigitalOcean vide jusqu’à une application Rails en production : un serveur, Docker Compose qui fait tourner votre application et Postgres, Caddy qui termine le HTTPS, une tâche de sauvegarde, et une façon répétable de livrer votre prochaine version.
Ce qu’il vous faut
- Un compte DigitalOcean et un Basic Droplet de taille
s-1vcpu-2gb(1 vCPU, 2 Go de mémoire, 50 Go SSD), actuellement à 12,00 $/mois. - Un nom de domaine sur lequel vous pouvez ajouter un enregistrement DNS.
- Une paire de clés SSH sur votre machine (
ssh-keygen -t ed25519si vous n’en avez pas). - Une application Rails, ou une nouvelle : ce guide en construit une à partir de zéro avec
rails new, donc vous pouvez suivre même sans application existante. - Une quinzaine de minutes pour laisser le serveur et le DNS se stabiliser, surtout à attendre plutôt qu’à taper.
Étape 1 : créer le serveur
Dans le panneau de contrôle DigitalOcean, choisissez Create, puis Droplets. Prenez Ubuntu 24.04 (LTS) x64 comme image, Basic comme plan, Regular SSD, et la taille s-1vcpu-2gb (12,00 $/mois, 2 Go de mémoire, 1 vCPU, 50 Go SSD). Choisissez une région proche de vos utilisateurs, Francfort ou Amsterdam si vous devez garder vos données dans l’Union européenne. Sous Authentication, choisissez SSH key et ajoutez la clé publique de votre machine plutôt qu’un mot de passe, puis créez le serveur.
Une fois démarré, DigitalOcean vous montre son IP publique. Connectez-vous pour confirmer l’accès :
ssh root@YOUR_DROPLET_IP
Vous devriez voir la bannière de bienvenue d’Ubuntu et atterrir sur un shell root. Si la connexion est refusée, vérifiez que la clé SSH ajoutée correspond bien à la clé privée sur votre machine.
Étape 2 : pointer votre domaine vers le serveur
Ajoutez un enregistrement A chez votre fournisseur DNS pour le nom d’hôte que vous voulez utiliser, pointant vers l’IP du serveur :
Type: A
Name: myapp (ou @ pour le domaine nu)
Value: YOUR_DROPLET_IP
TTL: 300
La propagation DNS peut prendre quelques minutes. Vérifiez-la depuis votre machine avant de continuer, puisque Caddy aura besoin que ça résolve déjà correctement à une étape plus loin :
dig +short myapp.example.com
Vous devriez voir s’afficher l’IP de votre serveur. Si rien ne s’affiche, attendez quelques minutes et réessayez avant de continuer.
Étape 3 : installer Docker sur le serveur
Le Dockerfile généré par Rails a besoin de Docker Engine et du plugin Compose sur le serveur. Le script d’installation de Docker gère les deux sur une machine Ubuntu neuve :
apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker
Ouvrez aussi le pare-feu pour SSH, HTTP et HTTPS, et rien d’autre :
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Vérifiez l’installation :
docker --version
docker compose version
Vous devriez voir une version de Docker et une version de Compose affichées, pas une erreur « command not found ».
Étape 4 : générer l’application Rails avec un Dockerfile et Postgres
Sur votre propre machine, générez une nouvelle application configurée pour Postgres plutôt que SQLite par défaut :
gem install rails -v 8.1.4
rails new myapp --database=postgresql
cd myapp
rails new crée un Dockerfile par défaut (c’est un des fichiers listés quand le générateur s’exécute, avec .dockerignore), ajusté pour la production : il construit vos gems et vos assets dans une étape, puis ne copie dans l’image finale que ce qui est nécessaire pour l’exécution. Rails 8 inclut aussi Thruster, un petit proxy HTTP devant Puma qui gère le HTTP/2, la compression et le X-Sendfile, ce qui explique pourquoi l’image générée expose le port 80 en interne et démarre avec ./bin/thrust ./bin/rails server plutôt que de lancer Puma directement. Vous n’atteindrez jamais ce port directement depuis Internet, Caddy sera devant.
Comme vous avez passé --database=postgresql, config/database.yml est configuré pour la gem pg plutôt que SQLite, et votre Gemfile.lock liste maintenant pg, ce qui est exactement ce qui indique à un hébergeur que cette application veut une base de données Postgres plutôt que MySQL ou SQLite.
Vérifiez que les fichiers sont là :
ls Dockerfile .dockerignore config/database.yml
Vous devriez voir les trois chemins affichés sans erreur.
Étape 5 : configurer la connexion à la base de données et les secrets
Ouvrez config/database.yml et faites pointer la section production vers une seule chaîne de connexion plutôt que des clés séparées d’hôte, de nom d’utilisateur et de mot de passe, puisque c’est ce que vous allez injecter depuis Docker Compose :
production:
<<: *default
url: <%= ENV.fetch("DATABASE_URL") %>
Rails chiffre config/credentials.yml.enc avec config/master.key, un fichier qui est volontairement exclu de git. Vous n’allez pas envoyer ce fichier sur le serveur. Lisez plutôt sa valeur une fois :
cat config/master.key
Sur le serveur, créez un répertoire pour l’application et un fichier .env avec cette clé et un mot de passe de base de données que vous générez vous-même :
mkdir -p /opt/myapp
cat > /opt/myapp/.env <<'EOF'
RAILS_MASTER_KEY=paste-the-value-from-config-master-key-here
POSTGRES_PASSWORD=a-long-random-password
EOF
chmod 600 /opt/myapp/.env
Vérifiez :
cat /opt/myapp/.env
Vous devriez voir les deux variables avec de vraies valeurs, pas des espaces réservés, et le fichier ne devrait pas être lisible par tout le monde (-rw------- avec ls -l).
Étape 6 : écrire le fichier Docker Compose
Copiez votre application sur le serveur (scp, rsync, ou un git clone de votre dépôt dans /opt/myapp), puis ajoutez compose.yml à côté :
services:
app:
build: .
image: myapp:latest
restart: unless-stopped
env_file: .env
environment:
RAILS_ENV: production
DATABASE_URL: postgres://myapp:${POSTGRES_PASSWORD}@db:5432/myapp_production
depends_on:
db:
condition: service_healthy
volumes:
- storage:/rails/storage
expose:
- "80"
db:
image: postgres:16.4-alpine
restart: unless-stopped
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: myapp_production
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
timeout: 5s
retries: 10
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
volumes:
pgdata:
storage:
caddy_data:
caddy_config:
Le service app se connecte à db par son nom de service, pas par localhost, puisque Compose met les deux conteneurs sur le même réseau privé. La ligne depends_on: condition: service_healthy empêche l’application de démarrer avant que Postgres puisse réellement accepter des connexions, pas juste avant que son conteneur existe. Les volumes nommés gardent votre base de données et vos fichiers Active Storage sur disque entre les redémarrages et les reconstructions.
Vérifiez que le fichier s’analyse correctement avant d’aller plus loin :
docker compose config
Vous devriez voir la configuration résolue affichée sans ligne d’erreur.
Étape 7 : ajouter Caddy comme proxy inverse avec HTTPS
Ajoutez un Caddyfile à côté de compose.yml :
myapp.example.com {
reverse_proxy app:80
}
Remplacez myapp.example.com par le domaine que vous avez pointé vers le serveur à l’étape 2. Caddy lit ce fichier et fait deux choses par lui-même : il relaie chaque requête vers le port 80 du conteneur app, et il demande un certificat TLS gratuit à Let’s Encrypt pour ce nom d’hôte exact, en le renouvelant automatiquement avant son expiration. Ça ne fonctionne que si le domaine résout déjà vers ce serveur, ce qui explique pourquoi le DNS vient avant Caddy dans ce guide.
Étape 8 : démarrer la pile
Depuis /opt/myapp :
docker compose build
docker compose up -d db
docker compose run --rm app bin/rails db:prepare
docker compose up -d
bin/rails db:prepare crée la base de données si elle n’existe pas encore et exécute les migrations en attente, la même commande que Railyard exécute avant chaque version plus loin dans ce guide.
Vérifiez la pile :
docker compose ps
curl -I https://myapp.example.com
Vous devriez voir les trois services à Up (et db à healthy), et le curl devrait retourner HTTP/2 200 sans avertissement de certificat.
Étape 9 : sauvegarder Postgres
Un dump pris en dehors du volume de données est ce qui vous protège réellement si le serveur disparaît. Ajoutez un script de sauvegarde :
cat > /opt/myapp/backup.sh <<'EOF'
#!/bin/sh
cd /opt/myapp
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec -T db pg_dump -U myapp myapp_production | gzip > /opt/myapp/backups/$TIMESTAMP.sql.gz
find /opt/myapp/backups -name '*.sql.gz' -mtime +14 -delete
EOF
mkdir -p /opt/myapp/backups
chmod +x /opt/myapp/backup.sh
Planifiez-le avec cron, chaque jour à 3 h :
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/myapp/backup.sh") | crontab -
Exécutez-le une fois manuellement et vérifiez que ça a fonctionné :
/opt/myapp/backup.sh
ls -lh /opt/myapp/backups
Vous devriez voir un fichier .sql.gz avec l’horodatage du jour et une taille non nulle. Pour autre chose qu’un projet personnel, copiez aussi ces fichiers en dehors du serveur, avec rclone vers DigitalOcean Spaces ou un autre entrepôt d’objets, pour qu’un serveur perdu ne signifie pas aussi une sauvegarde perdue.
Étape 10 : livrer une mise à jour
Chaque changement futur suit la même forme : amener le nouveau code sur le serveur, reconstruire l’image, exécuter les migrations en attente, puis remplacer le conteneur en cours d’exécution.
git pull
docker compose build app
docker compose run --rm app bin/rails db:prepare
docker compose up -d app
docker compose up -d app ne recrée que le conteneur app, pas db ni caddy, donc Postgres continue de tourner et l’état du certificat de Caddy n’est pas touché. Il y a un court écart, une seconde ou deux, pendant que l’ancien conteneur s’arrête et que le nouveau démarre, puisque cette configuration manuelle fait tourner un seul conteneur d’application. Vérifiez que c’est passé :
docker compose logs --tail=50 app
curl -I https://myapp.example.com
Vous devriez voir votre application démarrer proprement dans les logs et le curl retourner toujours 200 sur la nouvelle version.
Le raccourci : faites-le avec Railyard
Tout ce qui précède, le dimensionnement du serveur, Docker, Postgres, Caddy, les sauvegardes, les livraisons sans interruption, c’est aussi ce que Railyard automatise pour une application Rails sur DigitalOcean, sans que vous écriviez aucun des fichiers ci-dessus à la main.
Collez l’URL de votre dépôt dans https://app.railyard.run/deploy?repo= et choisissez DigitalOcean comme fournisseur. La première fois, vous allez générer un jeton dans le panneau de contrôle DigitalOcean (ouvrez API, puis générez un jeton d’accès personnel avec les droits de lecture et d’écriture) pour que Railyard puisse provisionner le serveur en votre nom. Railyard choisit par défaut le même serveur s-1vcpu-2gb pour cette combinaison que celui utilisé dans ce guide, et vous pouvez choisir n’importe quelle région DigitalOcean, dont Francfort ou Amsterdam si la résidence des données en Union européenne compte pour vous.
À partir de là, Railyard lit votre application de la même façon que ce guide l’a fait à la main : il voit pg dans votre Gemfile.lock et provisionne une base de données Postgres gérée plutôt que SQLite, il exécute bin/rails db:prepare avant chaque version, exactement comme l’étape 8 l’a fait manuellement, et si vous êtes sur Rails 8, il donne aux bases cache, queue et cable chacune leur propre chaîne de connexion plutôt que d’en partager une. Active Storage sur disque obtient un volume persistant automatiquement, comme le volume storage de l’étape 6. SECRET_KEY_BASE est généré une seule fois et conservé, donc vous n’avez jamais à le gérer vous-même. Si votre Gemfile.lock inclut Sidekiq, Solid Queue, GoodJob, Resque ou Delayed Job, Railyard l’offre comme worker en plus de votre processus web, sans service compose séparé à écrire. Le HTTPS et le proxy inverse de l’étape 7 sont gérés de la même façon, pointés vers votre domaine une fois que vous l’ajoutez.
Pour en savoir plus : déployer depuis un dépôt, bases de données et sauvegardes, le bouton de déploiement en un clic, démarrage rapide, le guide de référence Rails sur DigitalOcean, et comparaisons de coûts d’hébergement si vous voulez voir comment le coût du serveur seul ici se compare à un chemin géré. Le plan gratuit de Railyard couvre un serveur plus un serveur de base de données et trois applications sans carte requise, Pro est à 19 $/mois par équipe, et Team est à 49 $/mois par équipe.
Dépannage
Port déjà utilisé. Si docker compose up échoue avec quelque chose lié au port 80 ou 443, Apache ou Nginx par défaut d’Ubuntu est peut-être déjà installé et en marche. Vérifiez avec ss -tlnp | grep -E ':80|:443' et arrêtez ou retirez le service qui les occupe avant que Caddy n’essaie de s’y attacher.
Mauvais RAILS_MASTER_KEY ou mauvais identifiants. Une erreur comme ActiveSupport::MessageEncryptor::InvalidMessage au démarrage signifie presque toujours que le RAILS_MASTER_KEY dans /opt/myapp/.env ne correspond pas à la clé qui a chiffré config/credentials.yml.enc. Relancez cat config/master.key localement et comparez-le caractère par caractère avec ce qui est dans .env sur le serveur.
Connexion à Postgres refusée. Si les logs de l’application montrent could not connect to server: Connection refused, vérifiez que DATABASE_URL utilise db comme nom d’hôte, pas localhost ni 127.0.0.1. Dans le réseau de Compose, db est la seule adresse qui résout vers le conteneur Postgres; localhost désigne le conteneur app lui-même, qui n’a aucune base de données qui y tourne.
HTTPS qui n’est pas émis. Si docker compose logs caddy montre des erreurs de certificat répétées ou NXDOMAIN, votre enregistrement DNS de l’étape 2 n’a pas encore propagé ou pointe vers la mauvaise IP. Relancez dig +short myapp.example.com, confirmez que ça correspond au serveur, puis redémarrez Caddy avec docker compose restart caddy une fois que c’est bon.
Manque de mémoire pendant la précompilation des assets. Sur un serveur à 2 Go, docker compose build peut se faire tuer en plein milieu de la compilation des assets sur une application plus grosse. Ajoutez un fichier swap de 2 Go avant de construire : fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile, puis rendez-le permanent en l’ajoutant à /etc/fstab.
FAQ
Un serveur à 2 Go suffit-il pour la production ? Pour une application Rails de petite à moyenne taille avec un trafic modeste, oui, puisque Puma et Postgres tiennent ensemble confortablement, et la correction de swap du dépannage couvre le seul cas (gros builds d’assets) où 2 Go devient serré.
Puis-je garder SQLite au lieu de Postgres ? Oui, SQLite fonctionne bien sur un seul serveur tant que vous montez son fichier de base de données sur un volume persistant pour qu’il survive aux redémarrages de conteneur, mais Postgres vous donne un chemin plus propre si vous ajoutez un jour un deuxième serveur d’application.
Comment j’ajoute Redis ou Sidekiq ? Ajoutez un service redis dans compose.yml avec l’image officielle redis et son propre volume nommé, ajoutez REDIS_URL dans l’environnement de app pointant vers redis://redis:6379/0, et ajoutez un service worker qui exécute bundle exec sidekiq à partir de la même image plutôt que la commande du serveur Rails.
Est-ce que ça survit à un redémarrage du serveur ? Oui, restart: unless-stopped sur chaque service fait que Docker remonte toute la pile automatiquement après un redémarrage du serveur, tant que le service systemd de Docker lui-même est activé, ce que le script d’installation de l’étape 3 fait par défaut.
En quoi c’est différent d’utiliser Heroku ou Railway ? Ces plateformes gèrent le serveur pour vous à un prix par application; ce guide vous donne le même résultat sur un serveur que vous payez directement à DigitalOcean, au prix d’exécuter vous-même les commandes de mise à jour et de sauvegarde plutôt qu’un tableau de bord qui s’en charge.
Voilà une configuration de production complète : un serveur, Docker Compose qui fait tourner votre application et Postgres, Caddy qui gère le HTTPS, une sauvegarde nocturne, et une livraison répétable en trois commandes. Gardez ce compose.yml et ce Caddyfile dans votre dépôt avec l’application, et chaque futur déploiement sera le même git pull, build, migration, redémarrage que vous venez d’exécuter à l’étape 10.
Pourquoi les gens choisissent Railyard pour ça
- Votre propre compte infonuagique. Déployez sur DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou n'importe quel serveur Linux par SSH, facturé directement par votre fournisseur, sans majoration.
- Un prix fixe par équipe. L'offre gratuite est gratuite pour toujours : un serveur plus un serveur de base de données, trois applications, sans carte. Pro coûte 19 $ par mois pour jusqu'à cinq serveurs et un nombre illimité d'applications. Team coûte 49 $ par mois avec des serveurs illimités. Aucuns frais par application, par déploiement, par siège ou par gigaoctet.
- Des gabarits en un clic. Déployez des applications open source, comme des bases de données, des outils d'analyse et des outils de développement, sur votre propre serveur en un clic.
- Des bases de données gérées. Postgres, MySQL et Redis, avec des sauvegardes planifiées, vérifiées par restauration, et la récupération à un point dans le temps.
- Déploiement par push. Déployez depuis GitHub, GitLab ou la CLI, avec HTTPS automatique et des mises en production sans interruption.