Le mois dernier, un MacBook Pro de l'équipe s'est retrouvé transformé en « backend de secours » improvisé : il faisait tourner un service Swift Vapor destiné au débogage local de l'API pour une app iOS, lancé via swift run dans une fenêtre de terminal que personne n'osait fermer par peur de tout planter. Résultat : un redémarrage automatique après une mise à jour système, et le service s'est arrêté en silence pendant une bonne demi-journée. Le collègue QA, ne parvenant plus à joindre l'API, a d'abord cru à un problème de son propre réseau. Cette approche de « démon humain » doit absolument être abandonnée dès qu'on loue un Mac mini cloud censé rester en ligne en permanence. Cet article détaille le parcours complet pour faire sortir un service Vapor du terminal, le transformer en démon launchd résident, puis mettre en place des déploiements sans coupure.
Contexte : pourquoi héberger Vapor sur un Mac mini cloud
Swift Vapor est souvent utilisé comme « backend léger d'accompagnement pour un client iOS » : faux login, callbacks push, réception de tracking, back-office interne. Ce type de service reste de faible volume, mais doit rester disponible 24 h/24 et 7 j/7, sur un véritable environnement macOS — certains scénarios nécessitent de tester des certificats APNs ou de compiler des dépendances natives via swift build. Un Mac mini cloud, machine physique dédiée et non une VM, vient justement combler ce manque : une machine locale ne peut pas rester allumée en permanence, et une instance Linux cloud ne peut pas exécuter les dépendances spécifiques à macOS. Mais dès qu'on dispose d'une machine bare metal dédiée, toute la responsabilité de la gestion des processus retombe sur soi-même — aucune couche plateforme ne redémarre les processus plantés à votre place ; il faut mettre en place launchd soi-même.
Préparation de l'environnement : chaîne d'outils et plan de ports
Après connexion, commencer par vérifier les versions de la chaîne d'outils, afin d'éviter des écarts entre l'environnement de développement local et l'environnement cloud susceptibles d'affecter le comportement des artefacts compilés :
swift --version
xcode-select -p
mkdir -p ~/apps/vapor-api/releases
mkdir -p ~/apps/vapor-api/logs
Prévoir dès le départ deux ports lors de la planification, en anticipation du basculement sans coupure décrit plus loin :
| Usage | Port | Description |
|---|---|---|
| Port principal de production | 8080 | Version actuellement active, exposée via le reverse proxy Nginx |
| Port de préproduction / nouvelle version | 8081 | Le nouveau build s'y autovérifie d'abord |
| Vérification de santé interne | 8080/8081 /healthz |
Route Vapor personnalisée renvoyant le numéro de version du build |
Il est recommandé que la route /healthz renvoie directement le hash court du commit Git : cela permet, au moment du déploiement, de vérifier d'un coup d'œil que le basculement a bien pointé vers la bonne version, sans avoir à deviner si le processus a réellement redémarré.
Un démon launchd résident à la place de nohup
Créer le fichier ~/Library/LaunchAgents/com.m4rent.vaporapi.plist comme suit :
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.m4rent.vaporapi</string>
<key>ProgramArguments</key>
<array>
<string>/Users/deploy/apps/vapor-api/current/Run</string>
<string>serve</string>
<string>--hostname</string>
<string>127.0.0.1</string>
<string>--port</string>
<string>8080</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/deploy/apps/vapor-api/current</string>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/deploy/apps/vapor-api/logs/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/deploy/apps/vapor-api/logs/stderr.log</string>
</dict>
</plist>
Charger et vérifier :
launchctl load ~/Library/LaunchAgents/com.m4rent.vaporapi.plist
launchctl list | grep vaporapi
curl -s localhost:8080/healthz
Décryptage des champs clés du plist
Avec KeepAlive à true, launchd relance automatiquement le processus en cas de sortie anormale, sans qu'il soit nécessaire d'écrire un script de surveillance supplémentaire. Attention toutefois : si le fichier de configuration lui-même comporte une erreur provoquant un crash immédiat au démarrage, KeepAlive peut plonger le service dans une boucle de « redémarrage à la seconde » ; dans ce cas, consulter le nombre de redémarrages dans launchctl list permet de diagnostiquer le problème plus vite que de lire les logs. RunAtLoad garantit un démarrage automatique après boot ou connexion, ce qui, combiné à l'absence de fenêtre de maintenance planifiée sur un Mac mini cloud, dispense quasiment de toute intervention manuelle sur le processus de démarrage. WorkingDirectory doit toujours être défini explicitement, sous peine de voir les chemins relatifs des fichiers de configuration ou de logs mal résolus, le répertoire de travail par défaut de launchd n'étant pas celui attendu.
Déploiement sans coupure : double port + basculement via reverse proxy Nginx
Lors du déploiement d'une nouvelle version, ne surtout pas se contenter d'écraser le répertoire current puis de redémarrer le même processus — cela crée inévitablement quelques secondes de trou de service. Préférer l'approche « nouveau processus sur un nouveau port, bascule du trafic une fois la santé confirmée » :
cp -R releases/build-2026-07-12 releases/build-2026-07-12-verify
sed -i '' 's/8080/8081/' com.m4rent.vaporapi-staging.plist
launchctl load com.m4rent.vaporapi-staging.plist
curl -s localhost:8081/healthz
Une fois que /healthz renvoie le hash de commit attendu, basculer l'upstream Nginx :
upstream vapor_api {
server 127.0.0.1:8081;
}
nginx -s reload recharge uniquement la configuration sans interrompre les connexions déjà établies. L'ancien processus sur le port 8080 termine de traiter les requêtes en cours, puis est proprement arrêté via launchctl unload.
Ne surtout pas arrêter l'ancien port avant d'avoir basculé le trafic — un health check réussi ne garantit pas que la nouvelle version se comporte bien en charge réelle. Prévoir une fenêtre d'observation d'au moins quelques minutes : en cas d'anomalie, revenir à l'ancien upstream d'une seule commande est bien plus rapide qu'un rollback après coup.
Logs et supervision : éviter de saturer le disque
Les fichiers stdout.log/stderr.log redirigés par launchd ne sont pas rotés automatiquement par défaut : un service tournant en continu peut accumuler plusieurs Go en quelques mois. Utiliser l'outil système newsyslog en ajoutant une règle :
/Users/deploy/apps/vapor-api/logs/stdout.log deploy:staff 644 7 10240 * N
Cette règle indique une rotation dès que le log dépasse 10 Mo, avec un maximum de 7 fichiers d'historique conservés. La capacité SSD d'un Mac mini cloud étant fixe, des logs incontrôlés empiètent directement sur l'espace disponible pour les artefacts de build et les snapshots. Il est conseillé de surveiller chaque semaine la tendance de croissance avec du -sh ~/apps/vapor-api/logs.
Pièges à éviter
- Perte des variables d'environnement : un processus lancé par launchd n'hérite pas des variables
exportées dans.zshrc. Les chaînes de connexion à la base de données et autres paramètres doivent être placés dans le dictionnaireEnvironmentVariablesdu plist, ou chargés explicitement depuis un fichier.envséparé dans le code. - Occupation de port non détectée : si
launchctl loadrépond que « le service existe déjà » mais que le port reste inaccessible, il s'agit généralement d'une entrée plist fantôme laissée par un précédent crash. Faire d'abordlaunchctl remove, puis relancerload. - Boucle infinie liée à KeepAlive : un chemin de configuration erroné provoque un arrêt immédiat du processus ;
KeepAlivele relance alors indéfiniment, avec un pic de charge CPU. Commencer parlaunchctl unloadpour stopper l'hémorragie, puis investiguer. - Absence de point de retour arrière dans le script de déploiement : le répertoire
currentdoit idéalement être un lien symbolique vers un répertoirereleases/build-*précis. Un rollback consiste alors simplement à repointer le lien vers le répertoire précédent puis à recharger — éviter tout déploiement par écrasement direct.
Questions fréquentes
Pourquoi ne pas simplement lancer Vapor avec nohup ou dans un screen ?
nohup détache seulement le processus du terminal, mais ne le relance pas après un crash ou un redémarrage. launchd est le superviseur système de macOS : il peut relancer automatiquement via KeepAlive et démarrer le service au boot, ce qui compte sur une machine louée sur la durée.
Faut-il vraiment deux ports pour un déploiement sans coupure ?
Redémarrer sur le même port laisse toujours un intervalle où les connexions échouent avant que le nouveau processus soit prêt. Avec un second port, un contrôle de santé, puis une bascule de l'upstream Nginx, l'ancien processus termine les connexions en cours pendant que le nouveau reçoit le trafic.
La base de données doit-elle tourner sur le même Mac mini ?
Pour de petites charges, une instance locale PostgreSQL ou SQLite avec des snapshots quotidiens suffit. Pour des volumes plus importants ou plusieurs instances, mieux vaut se connecter à une base gérée séparément afin que le Mac mini ne gère que la couche applicative.
Testez-le sur un Mac mini dédié
Location à la journée, accès root complet, mise à disposition en quelques minutes : idéal pour valider avant de vous engager sur la durée.