crystal-deploy
= crystal-deploy :toc: left :toc-title: Table des matières :toclevels: 3 :source-highlighter: rouge :icons: font
Shard Crystal pour le déploiement d'applications https://kemalcr.com[Kemal] et https://martenframework.com[Marten] sur serveurs FreeBSD. Style Capistrano : releases/, current/, shared/. Génère dynamiquement les scripts rc.d et nginx.conf.
== Fonctionnalités
- Déploiement SSH depuis votre terminal local — aucune dépendance côté serveur
- Support natif Marten et Kemal via le champ
frameworkdansdeploy.yml - Structure Capistrano :
releases/,current/,shared/ - Génération dynamique du script
rc.dFreeBSD (avecdaemon(8)et redémarrage automatique) - Génération dynamique du
nginx.conf(mode pkg FreeBSD ou Passenger dans/opt) - Dialogue interactif pour construire le
.envlors de l'initialisation - Création automatique de l'utilisateur PostgreSQL et de la base de données
- Création du CNAME OVH via l'API REST (clés lues depuis le
.env) - Rollback en une commande (avec ré-application des migrations Marten)
- Conservation des N dernières releases (configurable)
- Compilation Crystal dans une session
tmux(résistante aux coupures SSH) - Génération automatique du workflow GitHub Actions (CI/CD) via
generate-ci - Arrêt gracieux (zero-downtime) : SIGTERM + attente de fin des requêtes avant bascule
== Installation
Ajoutez le shard dans votre shard.yml :
[source,yaml]
dependencies: crystal-deploy: github: aloli-crystal/crystal-deploy branch: developpement
Puis compilez le binaire deploy :
[source,sh]
shards install crystal build lib/crystal-deploy/src/deploy.cr --release -o bin/deploy
Ajoutez bin/deploy à votre .gitignore si vous ne souhaitez pas versionner le binaire.
NOTE: Le binaire deploy doit être compilé dans votre projet (pas dans le shard). Il lit config/deploy.yml depuis le répertoire courant.
== Configuration
Copiez le fichier d'exemple dans votre projet :
[source,sh]
cp lib/crystal-deploy/config/deploy.yml config/deploy.yml
Puis adaptez config/deploy.yml :
[source,yaml]
app_name: mon-app # <1> repo_url: git@github.com:aloli/mon-app.git crystal_main: src/server.cr keep_releases: 10
Framework Crystal utilisé : marten | kemal (défaut : kemal)
Adapte : variables .env, chemins assets NGINX, migrations, workflow CI
framework: marten # <2>
environments: preproduction: branch: developpement host: fiona.aloli.net user: deploy app_url: https://preprod.mon-app.aloli.app dns_subdomain: preprod.mon-app # <3> dns_target: fiona.aloli.net.
production: branch: production host: toby.aloli.net user: deploy app_url: https://mon-app.aloli.app
Variables du fichier .env côté serveur
Marten :
env_vars:
- key: SECRET_KEY label: "Clé secrète Marten" generate: hex32
- key: MARTEN_ALLOWED_HOSTS label: "MARTEN_ALLOWED_HOSTS" default_from: app_url
- key: MARTEN_SOCKET label: "MARTEN_SOCKET" default_from: socket_path
- key: DB_HOST label: "DB_HOST"
- key: DB_PORT label: "DB_PORT"
- key: DB_USER label: "DB_USER"
- key: DB_PASSWORD label: "DB_PASSWORD"
- key: DB_NAME label: "DB_NAME"
Kemal (exemple alternatif) :
env_vars:
- key: SESSION_SECRET
label: "SESSION_SECRET"
generate: hex32
- key: APP_URL
label: "APP_URL"
default_from: app_url
- key: DATABASE_URL
label: "DATABASE_URL"
build_from_pg: true
- key: UNIX_SOCKET
label: "UNIX_SOCKET"
default_from: socket_path
ovh: dns_zone: aloli.app
<1> Utilisé pour nommer les répertoires, services et binaires : mon-app--preproduction <2> Valeurs acceptées : marten ou kemal. Adapte NGINX, migrations, CI et variables .env <3> Optionnel — nécessite les clés API OVH dans votre .env local (voir section <
== Support Marten vs Kemal
Le champ framework dans config/deploy.yml adapte automatiquement le comportement du shard :
[cols="2,3,3"] |=== | Comportement | framework: kemal | framework: marten
| Variables .env NGINX | APP_URL + UNIX_SOCKET | MARTEN_ALLOWED_HOSTS + MARTEN_SOCKET
| Assets NGINX | /css/, /js/, /images/, /vendor/ | /assets/ → public/assets/
| Migrations | db/schema_pg.sql à l'init | marten migrate avant chaque activation
| Rollback migrations | Non applicable | marten migrate sur la release précédente
| Tests CI | DATABASE_URL + schema_pg.sql | DB_* + MARTEN_ENV=test + marten migrate
| Seed | ./bin/<app> seed | ./bin/<app> seed (si commande présente) |===
== Utilisation
=== Génération du workflow GitHub Actions
[source,sh]
bin/deploy generate-ci
Cette commande génère .github/workflows/deploy.yml, un workflow complet qui :
. Lance les tests sur chaque push (adapté au framework) . Déploie automatiquement sur pré-production à chaque push sur la branche developpement . Déploie automatiquement sur production à chaque push sur la branche production
=== Initialisation du serveur (une seule fois)
[source,sh]
bin/deploy init --preproduction bin/deploy init --production
Cette commande :
. Vérifie la connexion SSH . Propose un dialogue pour configurer le DNS OVH (optionnel, clés lues depuis .env) . Construit le fichier .env de façon interactive . Sur le serveur : crée l'utilisateur deploy, les répertoires, le .env, le nginx.conf
=== Déploiement
[source,sh]
bin/deploy deploy --preproduction bin/deploy deploy --production
Chaque déploiement :
. Clone la branche dans releases/<timestamp>/ . Lie shared/.env et shared/log/ . Compile le binaire Crystal en mode --release (dans tmux si disponible) . Marten uniquement : applique les migrations (marten migrate) . Arrêt gracieux de l'ancienne version (SIGTERM + attente de fin des requêtes) . Bascule le lien current vers la nouvelle release . Installe/met à jour le script rc.d . Démarre la nouvelle version . Recharge NGINX . Supprime les anciennes releases (au-delà de keep_releases)
=== Arrêt gracieux (zero-downtime)
Lors de chaque déploiement, le script envoie SIGTERM au processus en cours et attend qu'il se termine proprement avant de basculer vers la nouvelle release. Les requêtes HTTP en cours ne sont pas interrompues.
Le timeout d'attente est configurable via la variable d'environnement GRACEFUL_TIMEOUT (défaut : 30 secondes). Au-delà, un SIGKILL est envoyé en dernier recours.
Pour que l'application bénéficie de l'arrêt gracieux, elle doit intercepter SIGTERM.
Kemal :
[source,crystal]
Signal::TERM.trap do STDERR.puts "[SHUTDOWN] SIGTERM reçu — arrêt gracieux en cours..." spawn { Kemal.stop rescue nil } end
Marten : l'arrêt gracieux est géré nativement par Marten::Server.
=== Rollback
[source,sh]
bin/deploy rollback --production
Pour Marten, les migrations sont ré-appliquées sur la release précédente après le rollback.
=== Statut
[source,sh]
bin/deploy status --preproduction
== Structure sur le serveur
[source]
/home/mon-app--preproduction/ ├── releases/ │ ├── 20260304_143022/ ← release horodatée │ │ ├── bin/mon-app--preproduction │ │ └── ... │ └── 20260301_091500/ ├── current -> releases/20260304_143022/ └── shared/ ├── .env ← persistant entre les deploys ├── nginx.conf ← généré par init └── log/
/tmp/.mon-app--preproduction.pid ← pidfile (convention /tmp comme PostgreSQL) /tmp/.mon-app--preproduction.sock ← socket Unix (deploy:www 660)
/usr/local/etc/rc.d/mon-app--preproduction /usr/local/bin/mon-app--preproduction -> current/bin/...
== NGINX
Le fichier shared/nginx.conf est généré lors de l'init. Il est automatiquement lié selon le mode détecté :
[cols="1,2,2"] |=== | Mode | Lien créé | Directive à ajouter dans nginx.conf principal
| Passenger (/opt/websites/ présent) | /opt/websites/mon-app--preproduction.conf | include /opt/websites/*.conf;
| pkg FreeBSD | sites-enabled/mon-app--preproduction | include sites-enabled/*; |===
Le socket Unix est créé avec les permissions deploy:www 660 pour que NGINX (qui tourne sous l'utilisateur www) puisse y accéder.
[#dns-ovh] == Gestion des fichiers de configuration
L'architecture du shard repose sur une séparation stricte des responsabilités entre les fichiers de configuration. Il est crucial de bien comprendre le rôle de chacun :
[cols="2,3,1"] |=== | Fichier | Rôle | Versionné
| config/deploy.yml | Source unique de vérité pour l'infrastructure. Contient la définition des environnements (hôtes, branches, URLs, sous-domaines DNS, framework utilisé). | Oui
| .env local | Secrets du développeur. Contient les clés d'accès aux API externes (comme l'API OVH) nécessaires depuis la machine locale pour administrer l'infrastructure. Ne doit jamais contenir la configuration des serveurs. | Non
| shared/.env (serveur) | Secrets de l'application en production. Construit interactivement lors de l'init. Contient les identifiants de base de données, clés secrètes Marten/Kemal, et identifiants SMTP. Lu uniquement par l'application sur le serveur. | Non |===
[WARNING]
Ne dupliquez jamais les informations de config/deploy.yml (comme les URLs ou les hôtes) dans votre .env local. Le binaire deploy lit directement le fichier YAML pour orchestrer les déploiements.
[#dns-ovh] == DNS OVH (optionnel)
=== Génération guidée des clés API
Le shard propose un assistant interactif pour créer et configurer les clés API OVH en une seule commande :
[source,sh]
bin/deploy ovh-setup
Cet assistant :
. Ouvre l'URL de création d'application OVH et attend votre confirmation . Demande l'Application Key et l'Application Secret obtenus . Appelle automatiquement l'API OVH pour générer le Consumer Key avec les droits DNS minimaux . Ouvre l'URL de validation des droits et attend votre confirmation . Sauvegarde les trois clés dans votre .env local avec les permissions 600
Si des clés existent déjà dans le .env, l'assistant vous propose de les remplacer avant de continuer.
Le shard peut créer automatiquement le CNAME DNS lors de l'initialisation, en utilisant l'API REST OVH.
=== Clés OVH dans le .env local
Les clés API OVH sont lues depuis votre .env local (à la racine de votre projet), dans cet ordre de priorité :
[cols="1,3"] |=== | Source | Description
| Variables d'environnement shell | export OVH_APP_KEY=... dans votre shell
| .env local (recommandé) | Fichier .env à la racine du projet — ne pas versionner
| config/.ovhrc (legacy) | Ancien emplacement, conservé pour compatibilité. Un avertissement invite à migrer vers .env. |===
Ajoutez les trois variables dans votre .env local :
[source,sh]
Clés API OVH — NE PAS VERSIONNER
OVH_APP_KEY=votre_app_key OVH_APP_SECRET=votre_app_secret OVH_CONSUMER_KEY=votre_consumer_key
[IMPORTANT]
Vérifiez que .env est bien dans votre .gitignore :
[source,sh]
echo ".env" >> .gitignore
====
Si les clés sont absentes du .env, le dialogue interactif les demande lors de l'init et les sauvegarde automatiquement dans votre .env local.
=== Génération des clés OVH
Étape 1 — Créez l'application sur https://eu.api.ovh.com/createApp/
Vous obtenez une Application Key et un Application Secret.
Étape 2 — Générez le Consumer Key :
[source,sh]
curl -X POST https://eu.api.ovh.com/1.0/auth/credential
-H 'Content-Type: application/json'
-H 'X-Ovh-Application: VOTRE_APP_KEY'
-d '{ "accessRules": [ {"method": "GET", "path": "/domain/zone/aloli.app"}, {"method": "GET", "path": "/domain/zone/aloli.app/record"}, {"method": "POST", "path": "/domain/zone/aloli.app/record"}, {"method": "POST", "path": "/domain/zone/aloli.app/refresh"} ], "redirection": "https://www.ovhcloud.com/fr/" }'
Ouvrez la validationUrl retournée dans votre navigateur pour valider.
Étape 3 — Ajoutez les trois clés dans votre .env local (voir ci-dessus).
=== Configuration dans deploy.yml
Pour activer la création automatique du CNAME, ajoutez dans chaque environnement concerné :
[source,yaml]
environments: preproduction: branch: developpement host: fiona.aloli.net user: deploy app_url: https://preprod.mon-app.aloli.app dns_subdomain: preprod.mon-app # ← sous-domaine à créer dns_target: fiona.aloli.net. # ← cible du CNAME (point final obligatoire)
ovh: dns_zone: aloli.app # ← zone DNS OVH
== GitHub Actions (CI/CD automatique)
La commande generate-ci génère automatiquement le fichier de workflow adapté à votre configuration et à votre framework.
=== Étape 1 — Générer la clé SSH dédiée
[source,sh]
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_ed25519 -N ""
Autoriser la clé publique sur le serveur
ssh-copy-id -i ~/.ssh/deploy_ed25519.pub deploy@votre-serveur.net
=== Étape 2 — Configurer les secrets GitHub
Dans les paramètres du dépôt GitHub → Settings → Secrets and variables → Actions :
[cols="1,3"] |=== | Secret | Valeur
| SSH_PRIVATE_KEY | Contenu de ~/.ssh/deploy_ed25519 (clé privée) |===
=== Étape 3 — Générer le workflow
[source,sh]
bin/deploy generate-ci git add .github/workflows/deploy.yml git commit -m "ci: ajouter le workflow de déploiement automatique" git push
Le workflow généré déclenche automatiquement un déploiement à chaque push sur les branches developpement (pré-production) et production.
== Licence
MIT — voir link:LICENSE[LICENSE]
crystal-deploy
- 0
- 0
- 0
- 0
- 0
- about 4 hours ago
- March 4, 2026
MIT License
Wed, 25 Mar 2026 17:24:56 GMT