Déployer automatiquement des applications Web à l'aide des webhooks GitHub
Publié: 2022-03-11Quiconque développe des applications Web et tente de les exécuter sur ses propres serveurs non gérés est conscient du processus fastidieux impliqué dans le déploiement de son application et la mise en place de futures mises à jour. Les fournisseurs de plate-forme en tant que service (PaaS) ont facilité le déploiement d'applications Web sans avoir à passer par le processus de provisionnement et de configuration de serveurs individuels, en échange d'une légère augmentation des coûts et d'une diminution de la flexibilité. Le PaaS a peut-être facilité les choses, mais parfois nous devons ou voulons toujours déployer des applications sur nos propres serveurs non gérés. L'automatisation de ce processus de déploiement d'applications Web sur votre serveur peut sembler écrasante au début, mais en réalité, trouver un outil simple pour automatiser cela peut être plus facile que vous ne le pensez. La facilité de mise en œuvre de cet outil dépend beaucoup de la simplicité de vos besoins, mais ce n'est certainement pas difficile à réaliser, et peut probablement aider à économiser beaucoup de temps et d'efforts en faisant les fastidieux éléments répétitifs de l'application Web. déploiements.
De nombreux développeurs ont trouvé leurs propres moyens d'automatiser les processus de déploiement de leurs applications Web. Étant donné que la façon dont vous déployez vos applications Web dépend beaucoup de la pile technologique exacte utilisée, ces solutions d'automatisation varient les unes des autres. Par exemple, les étapes impliquées dans le déploiement automatique d'un site Web PHP sont différentes du déploiement d'une application Web Node.js. D'autres solutions existent, telles que Dokku, qui sont assez génériques et ces éléments (appelés buildpacks) fonctionnent bien avec une gamme plus large de piles technologiques.
Dans ce didacticiel, nous examinerons les idées fondamentales derrière un outil simple que vous pouvez créer pour automatiser vos déploiements d'applications Web à l'aide de webhooks, de buildpacks et de Procfiles GitHub. Le code source du programme prototype que nous allons explorer dans cet article est disponible sur GitHub.
Premiers pas avec les applications Web
Pour automatiser le déploiement de notre application Web, nous allons écrire un programme Go simple. Si vous n'êtes pas familier avec Go, n'hésitez pas à suivre, car les constructions de code utilisées tout au long de cet article sont assez simples et devraient être faciles à comprendre. Si vous en avez envie, vous pouvez probablement porter tout le programme dans une langue de votre choix assez facilement.
Avant de commencer, assurez-vous que la distribution Go est installée sur votre système. Pour installer Go, vous pouvez suivre les étapes décrites dans la documentation officielle.
Ensuite, vous pouvez télécharger le code source de cet outil en clonant le référentiel GitHub. Cela devrait vous faciliter la tâche, car les extraits de code de cet article sont étiquetés avec leurs noms de fichiers correspondants. Si vous le souhaitez, vous pouvez l'essayer tout de suite.
L'un des principaux avantages de l'utilisation de Go pour ce programme est que nous pouvons le construire de manière à avoir un minimum de dépendances externes. Dans notre cas, pour exécuter ce programme sur un serveur, nous devons simplement nous assurer que Git et Bash sont installés. Étant donné que les programmes Go sont compilés dans des fichiers binaires liés statiquement, vous pouvez compiler le programme sur votre ordinateur, le télécharger sur le serveur et l'exécuter avec presque aucun effort. Pour la plupart des autres langages populaires d'aujourd'hui, cela nécessiterait un environnement d'exécution ou un interpréteur gigantesque installé sur le serveur uniquement pour exécuter votre automate de déploiement. Les programmes Go, lorsqu'ils sont bien faits, peuvent également être très faciles à utiliser sur le processeur et la RAM - ce que vous attendez de programmes comme celui-ci.
Webhooks GitHub
Avec GitHub Webhooks, il est possible de configurer votre référentiel GitHub pour émettre des événements chaque fois que quelque chose change dans le référentiel ou qu'un utilisateur effectue des actions particulières sur le référentiel hébergé. Cela permet aux utilisateurs de s'abonner à ces événements et d'être avertis via des invocations d'URL des différents événements qui se déroulent autour de votre référentiel.
Créer un webhook est très simple :
- Accédez à la page des paramètres de votre référentiel
- Cliquez sur "Webhooks & Services" dans le menu de navigation de gauche
- Cliquez sur le bouton "Ajouter un webhook"
- Définissez une URL, et éventuellement un secret (qui permettra au destinataire de vérifier la charge utile)
- Faire d'autres choix sur le formulaire, au besoin
- Soumettez le formulaire en cliquant sur le bouton vert "Ajouter un webhook"
GitHub fournit une documentation complète sur les Webhooks et leur fonctionnement exact, quelles informations sont fournies dans la charge utile en réponse à divers événements, etc. Pour les besoins de cet article, nous nous intéressons particulièrement à l'événement "push" qui est émis chaque fois pousse vers n'importe quelle branche du référentiel.
Packs de construction
Les Buildpacks sont à peu près standard de nos jours. Utilisés par de nombreux fournisseurs PaaS, les buildpacks vous permettent de spécifier comment la pile sera configurée avant le déploiement d'une application. Écrire des packs de construction pour votre application Web est vraiment facile, mais le plus souvent, une recherche rapide sur le Web peut vous trouver un pack de construction que vous pouvez utiliser pour votre application Web sans aucune modification.
Si vous avez déployé une application sur PaaS comme Heroku, vous savez peut-être déjà ce que sont les buildpacks et où les trouver. Heroku a une documentation complète sur la structure des packs de construction et une liste de quelques packs de construction populaires bien construits.
Notre programme d'automatisation utilisera un script de compilation pour préparer l'application avant de la lancer. Par exemple, une construction Node.js par Heroku analyse le fichier package.json, télécharge une version appropriée de Node.js et télécharge les dépendances NPM pour l'application. Il convient de noter que pour garder les choses simples, nous n'aurons pas de support étendu pour les buildpacks dans notre programme prototype. Pour l'instant, nous supposerons que les scripts buildpack sont écrits pour être exécutés avec Bash et qu'ils s'exécuteront sur une nouvelle installation Ubuntu telle quelle. Si nécessaire, vous pouvez facilement l'étendre à l'avenir pour répondre à des besoins plus ésotériques.
Profils
Les procfiles sont de simples fichiers texte qui vous permettent de définir les différents types de processus que vous avez dans votre application. Pour la plupart des applications simples, vous auriez idéalement un seul processus "web" qui serait le processus qui gère les requêtes HTTP.
L'écriture de profils est facile. Définissez un type de processus par ligne en tapant son nom, suivi de deux-points, suivi de la commande qui lancera le processus :
<type>: <command>
Par exemple, si vous travailliez avec une application Web basée sur Node.js, pour démarrer le serveur Web, vous exécuteriez la commande « node index.js ». Vous pouvez simplement créer un Procfile dans le répertoire de base du code et le nommer "Procfile" avec ce qui suit :
web: node index.js
Nous exigerons que les applications définissent les types de processus dans Procfiles afin que nous puissions les démarrer automatiquement après avoir extrait le code.
Gestion des événements
Dans notre programme, nous devons inclure un serveur HTTP qui nous permettra de recevoir les requêtes POST entrantes de GitHub. Nous devrons dédier un chemin d'URL pour gérer ces demandes de GitHub. La fonction qui gérera ces charges utiles entrantes ressemblera à ceci :
// hook.go type HookOptions struct { App *App Secret string } func NewHookHandler(o *HookOptions) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { evName := r.Header.Get("X-Github-Event") if evName != "push" { log.Printf("Ignoring '%s' event", evName) return } body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if o.Secret != "" { ok := false for _, sig := range strings.Fields(r.Header.Get("X-Hub-Signature")) { if !strings.HasPrefix(sig, "sha1=") { continue } sig = strings.TrimPrefix(sig, "sha1=") mac := hmac.New(sha1.New, []byte(o.Secret)) mac.Write(body) if sig == hex.EncodeToString(mac.Sum(nil)) { ok = true break } } if !ok { log.Printf("Ignoring '%s' event with incorrect signature", evName) return } } ev := github.PushEvent{} err = json.Unmarshal(body, &ev) if err != nil { log.Printf("Ignoring '%s' event with invalid payload", evName) http.Error(w, "Bad Request", http.StatusBadRequest) return } if ev.Repo.FullName == nil || *ev.Repo.FullName != o.App.Repo { log.Printf("Ignoring '%s' event with incorrect repository name", evName) http.Error(w, "Bad Request", http.StatusBadRequest) return } log.Printf("Handling '%s' event for %s", evName, o.App.Repo) err = o.App.Update() if err != nil { return } }) }
Nous commençons par vérifier le type d'événement qui a généré cette charge utile. Puisque nous ne sommes intéressés que par l'événement "push", nous pouvons ignorer tous les autres événements. Même si vous configurez le webhook pour n'émettre que des événements "push", il y aura toujours au moins un autre type d'événement que vous pouvez vous attendre à recevoir sur votre point de terminaison de crochet : "ping". Le but de cet événement est de déterminer si le webhook a été correctement configuré sur GitHub.
Ensuite, nous lisons le corps entier de la requête entrante, calculons son HMAC-SHA1 en utilisant le même secret que nous utiliserons pour configurer notre webhook, et déterminons la validité de la charge utile entrante en la comparant avec la signature incluse dans l'en-tête du demande. Dans notre programme, nous ignorons cette étape de validation si le secret n'est pas configuré. D'un autre côté, ce n'est peut-être pas une bonne idée de lire tout le corps sans avoir au moins une sorte de limite supérieure sur la quantité de données que nous voudrons traiter ici, mais gardons les choses simples pour nous concentrer sur les aspects critiques de cet outil.
Ensuite, nous utilisons une structure de la bibliothèque cliente GitHub pour Go afin de démarshaler la charge utile entrante. Puisque nous savons qu'il s'agit d'un événement "push", nous pouvons utiliser la structure PushEvent. Nous utilisons ensuite la bibliothèque d'encodage JSON standard pour démarshaler la charge utile dans une instance de la structure. Nous effectuons quelques vérifications d'intégrité, et si tout va bien, nous invoquons la fonction qui commence à mettre à jour notre application.
Application de mise à jour
Une fois que nous recevons une notification d'événement sur notre point de terminaison de webhook, nous pouvons commencer à mettre à jour notre application. Dans cet article, nous allons nous intéresser à une implémentation assez simple de ce mécanisme, et il y aura certainement place à des améliorations. Cependant, cela devrait nous permettre de démarrer avec un processus de déploiement automatisé de base.
Initialisation du référentiel local
Ce processus commencera par une simple vérification pour déterminer si c'est la première fois que nous essayons de déployer l'application. Nous le ferons en vérifiant si le répertoire du référentiel local existe. S'il n'existe pas, nous allons d'abord initialiser notre référentiel local :
// app.go func (a *App) initRepo() error { log.Print("Initializing repository") err := os.MkdirAll(a.repoDir, 0755) // Check err cmd := exec.Command("git", "--git-dir="+a.repoDir, "init") cmd.Stderr = os.Stderr err = cmd.Run() // Check err cmd = exec.Command("git", "--git-dir="+a.repoDir, "remote", "add", "origin", fmt.Sprintf("[email protected]:%s.git", a.Repo)) cmd.Stderr = os.Stderr err = cmd.Run() // Check err return nil }
Cette méthode sur la structure App peut être utilisée pour initialiser le référentiel local, et ses mécanismes sont extrêmement simples :
- Créez un répertoire pour le référentiel local s'il n'existe pas.
- Utilisez la commande "git init" pour créer un référentiel nu.
- Ajoutez une URL pour le référentiel distant à notre référentiel local et nommez-le "origine".
Une fois que nous avons un référentiel initialisé, la récupération des modifications devrait être simple.
Récupération des modifications
Pour récupérer les modifications du dépôt distant, nous avons juste besoin d'invoquer une commande :
// app.go func (a *App) fetchChanges() error { log.Print("Fetching changes") cmd := exec.Command("git", "--git-dir="+a.repoDir, "fetch", "-f", "origin", "master:master") cmd.Stderr = os.Stderr return cmd.Run() }
En faisant un "git fetch" pour notre référentiel local de cette manière, nous pouvons éviter les problèmes avec Git qui ne peut pas avancer rapidement dans certains scénarios. Non pas que les récupérations forcées soient une chose sur laquelle vous devriez compter, mais si vous devez effectuer une poussée forcée vers votre référentiel distant, cela le gérera avec grâce.

Compilation de l'application
Étant donné que nous utilisons des scripts de buildpacks pour compiler nos applications en cours de déploiement, notre tâche ici est relativement simple :
// app.go func (a *App) compileApp() error { log.Print("Compiling application") _, err := os.Stat(a.appDir) if !os.IsNotExist(err) { err = os.RemoveAll(a.appDir) // Check err } err = os.MkdirAll(a.appDir, 0755) // Check err cmd := exec.Command("git", "--git-dir="+a.repoDir, "--work-tree="+a.appDir, "checkout", "-f", "master") cmd.Dir = a.appDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err buildpackDir, err := filepath.Abs("buildpack") // Check err cmd = exec.Command("bash", filepath.Join(buildpackDir, "bin", "detect"), a.appDir) cmd.Dir = buildpackDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err cacheDir, err := filepath.Abs("cache") // Check err err = os.MkdirAll(cacheDir, 0755) // Check err cmd = exec.Command("bash", filepath.Join(buildpackDir, "bin", "compile"), a.appDir, cacheDir) cmd.Dir = a.appDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() }
Nous commençons par supprimer notre répertoire d'application précédent (le cas échéant). Ensuite, nous en créons un nouveau et extrayons le contenu de la branche master. Nous utilisons ensuite le script "detect" du buildpack configuré pour déterminer si l'application est quelque chose que nous pouvons gérer. Ensuite, nous créons un répertoire "cache" pour le processus de compilation du buildpack si nécessaire. Étant donné que ce répertoire persiste d'une génération à l'autre, il se peut que nous n'ayons pas à créer un nouveau répertoire car il en existera déjà un lors d'un processus de compilation précédent. À ce stade, nous pouvons invoquer le script "compile" du buildpack et lui faire préparer tout le nécessaire pour l'application avant le lancement. Lorsque les buildpacks sont exécutés correctement, ils peuvent gérer eux-mêmes la mise en cache et la réutilisation des ressources précédemment mises en cache.
Redémarrage de l'application
Dans notre implémentation de ce processus de déploiement automatisé, nous allons arrêter les anciens processus avant de démarrer le processus de compilation, puis démarrer les nouveaux processus une fois la phase de compilation terminée. Bien que cela facilite la mise en œuvre de l'outil, cela laisse des moyens potentiellement étonnants d'améliorer le processus de déploiement automatisé. Pour améliorer ce prototype, vous pouvez probablement commencer par garantir l'absence de temps d'arrêt lors des mises à jour. Pour l'instant, nous allons continuer avec l'approche la plus simple :
// app.go func (a *App) stopProcs() error { log.Print(".. stopping processes") for _, n := range a.nodes { err := n.Stop() if err != nil { return err } } return nil } func (a *App) startProcs() error { log.Print("Starting processes") err := a.readProcfile() if err != nil { return err } for _, n := range a.nodes { err = n.Start() if err != nil { return err } } return nil }
Dans notre prototype, nous arrêtons et démarrons les différents processus en itérant sur un tableau de nœuds, où chaque nœud est un processus correspondant à une des instances de l'application (telle que configurée avant de lancer cet outil sur le serveur). Dans notre outil, nous gardons une trace de l'état actuel du processus pour chaque nœud. Nous maintenons également des fichiers journaux individuels pour eux. Avant que tous les nœuds ne soient démarrés, chacun se voit attribuer un port unique à partir d'un numéro de port donné :
// node.go func NewNode(app *App, name string, no int, port int) (*Node, error) { logFile, err := os.OpenFile(filepath.Join(app.logsDir, fmt.Sprintf("%s.%d.txt", name, no)), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return nil, err } n := &Node{ App: app, Name: name, No: no, Port: port, stateCh: make(chan NextState), logFile: logFile, } go func() { for { next := <-n.stateCh if n.State == next.State { if next.doneCh != nil { close(next.doneCh) } continue } switch next.State { case StateUp: log.Printf("Starting process %s.%d", n.Name, n.No) cmd := exec.Command("bash", "-c", "for f in .profile.d/*; do source $f; done; "+n.Cmd) cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", n.App.appDir)) cmd.Env = append(cmd.Env, fmt.Sprintf("PORT=%d", n.Port)) cmd.Env = append(cmd.Env, n.App.Env...) cmd.Dir = n.App.appDir cmd.Stdout = n.logFile cmd.Stderr = n.logFile err := cmd.Start() if err != nil { log.Printf("Process %s.%d exited", n.Name, n.No) n.State = StateUp } else { n.Process = cmd.Process n.State = StateUp } if next.doneCh != nil { close(next.doneCh) } go func() { err := cmd.Wait() if err != nil { log.Printf("Process %s.%d exited", n.Name, n.No) n.stateCh <- NextState{ State: StateDown, } } }() case StateDown: log.Printf("Stopping process %s.%d", n.Name, n.No) if n.Process != nil { n.Process.Kill() n.Process = nil } n.State = StateDown if next.doneCh != nil { close(next.doneCh) } } } }() return n, nil } func (n *Node) Start() error { n.stateCh <- NextState{ State: StateUp, } return nil } func (n *Node) Stop() error { doneCh := make(chan int) n.stateCh <- NextState{ State: StateDown, doneCh: doneCh, } <-doneCh return nil }
En un coup d'œil, cela peut sembler un peu plus compliqué que ce que nous avons fait jusqu'à présent. Pour faciliter la compréhension des choses, décomposons le code ci-dessus en quatre parties. Les deux premiers sont dans la fonction "NewNode". Lorsqu'il est appelé, il remplit une instance de la structure "Node" et génère une routine Go qui permet de démarrer et d'arrêter le processus correspondant à ce nœud. Les deux autres sont les deux méthodes sur la structure "Node": "Start" et "Stop". Un processus est démarré ou arrêté en transmettant un "message" via un canal particulier que cette routine Go par nœud surveille. Vous pouvez soit transmettre un message pour démarrer le processus, soit un message différent pour l'arrêter. Étant donné que les étapes réelles impliquées dans le démarrage ou l'arrêt d'un processus se produisent dans une seule routine Go, il n'y a aucune chance d'obtenir des conditions de course.
La routine Go démarre une boucle infinie où elle attend un "message" via le canal "stateCh". Si le message transmis à ce canal demande au nœud de démarrer le processus (à l'intérieur de "case StateUp"), il utilise Bash pour exécuter la commande. Ce faisant, il configure la commande pour utiliser les variables d'environnement définies par l'utilisateur. Il redirige également les sorties standard et les flux d'erreurs vers un fichier journal prédéfini.
D'autre part, pour arrêter un processus (à l'intérieur du "case StateDown"), il le tue simplement. C'est là que vous pourriez probablement faire preuve de créativité, et au lieu de tuer le processus, envoyez-lui immédiatement un SIGTERM et attendez quelques secondes avant de le tuer, donnant au processus une chance de s'arrêter gracieusement.
Les méthodes "Démarrer" et "Arrêter" facilitent la transmission du message approprié au canal. Contrairement à la méthode "Start", la méthode "Stop" attend en fait que les processus soient tués avant de revenir. "Démarrer" transmet simplement un message au canal pour démarrer le processus et revient.
Tout combiner
Enfin, tout ce que nous devons faire est de tout relier à la fonction principale du programme. C'est là que nous allons charger et analyser le fichier de configuration, mettre à jour le buildpack, tenter de mettre à jour notre application une fois et démarrer le serveur Web pour écouter les charges utiles d'événements "push" entrantes de GitHub :
// main.go func main() { cfg, err := toml.LoadFile("config.tml") catch(err) url, ok := cfg.Get("buildpack.url").(string) if !ok { log.Fatal("buildpack.url not defined") } err = UpdateBuildpack(url) catch(err) // Read configuration options into variables repo (string), env ([]string) and procs (map[string]int) // ... app, err := NewApp(repo, env, procs) catch(err) err = app.Update() catch(err) secret, _ := cfg.Get("hook.secret").(string) http.Handle("/hook", NewHookHandler(&HookOptions{ App: app, Secret: secret, })) addr, ok := cfg.Get("core.addr").(string) if !ok { log.Fatal("core.addr not defined") } err = http.ListenAndServe(addr, nil) catch(err) }
Étant donné que nous exigeons que les buildpacks soient de simples référentiels Git, "UpdateBuildpack" (implémenté dans buildpack.go) effectue simplement un "git clone" et un "git pull" si nécessaire avec l'URL du référentiel pour mettre à jour la copie locale du buildpack.
Essayer
Si vous n'avez pas encore cloné le référentiel, vous pouvez le faire maintenant. Si vous avez installé la distribution Go, il devrait être possible de compiler le programme immédiatement.
mkdir hopper cd hopper export GOPATH=`pwd` go get github.com/hjr265/toptal-hopper go install github.com/hjr265/toptal-hopper
Cette séquence de commandes va créer un répertoire nommé hopper, le définir comme GOPATH, récupérer le code de GitHub avec les bibliothèques Go nécessaires et compiler le programme dans un binaire que vous pouvez trouver dans le répertoire "$GOPATH/bin". Avant de pouvoir l'utiliser sur un serveur, nous devons créer une application Web simple pour tester cela. Pour plus de commodité, j'ai créé une simple application Web Node.js de type "Hello, world" et l'ai téléchargée dans un autre référentiel GitHub que vous pouvez bifurquer et réutiliser pour ce test. Ensuite, nous devons télécharger le binaire compilé sur un serveur et créer un fichier de configuration dans le même répertoire :
# config.tml [core] addr = ":26590" [buildpack] url = "https://github.com/heroku/heroku-buildpack-nodejs.git" [app] repo = "hjr265/hopper-hello.js" [app.env] GREETING = "Hello" [app.procs] web = 1 [hook] secret = ""
La première option de notre fichier de configuration, « core.addr », est ce qui nous permet de configurer le port HTTP du serveur Web interne de notre programme. Dans l'exemple ci-dessus, nous l'avons défini sur ": 26590", ce qui obligera le programme à écouter les charges utiles d'événements "push" sur "http://{host}:26590/hook". Lors de la configuration du webhook GitHub, remplacez simplement "{host}" par le nom de domaine ou l'adresse IP qui pointe vers votre serveur. Assurez-vous que le port est ouvert au cas où vous utiliseriez une sorte de pare-feu.
Ensuite, nous choisissons un buildpack en définissant son URL Git. Ici, nous utilisons le buildpack Node.js de Heroku.
Sous "app", nous définissons "repo" sur le nom complet de votre référentiel GitHub hébergeant le code de l'application. Puisque j'héberge l'exemple d'application sur "https://github.com/hjr265/hopper-hello.js", le nom complet du référentiel est "hjr265/hopper-hello.js".
Ensuite, nous définissons des variables d'environnement pour l'application et le nombre de chaque type de processus dont nous avons besoin. Et enfin, nous choisissons un secret, afin de pouvoir vérifier les charges utiles d'événements "push" entrants.
Nous pouvons maintenant démarrer notre programme d'automatisation sur le serveur. Si tout est correctement configuré (y compris le déploiement des clés SSH, afin que le référentiel soit accessible depuis le serveur), le programme doit récupérer le code, préparer l'environnement à l'aide du buildpack et lancer l'application. Il ne nous reste plus qu'à configurer un webhook dans le référentiel GitHub pour émettre des événements push et le pointer vers "http://{host}:26590/hook". Assurez-vous de remplacer "{host}" par le nom de domaine ou l'adresse IP qui pointe vers votre serveur.
Pour enfin le tester, apportez quelques modifications à l'exemple d'application et envoyez-les à GitHub. Vous remarquerez que l'outil d'automatisation entrera immédiatement en action et mettra à jour le référentiel sur le serveur, compilera l'application et la redémarrera.
Conclusion
D'après la plupart de nos expériences, nous pouvons dire que c'est quelque chose de très utile. L'application prototype que nous avons préparée dans cet article n'est peut-être pas quelque chose que vous voudrez utiliser sur un système de production tel quel. Il y a une tonne de place à l'amélioration. Un outil comme celui-ci devrait avoir une meilleure gestion des erreurs, prendre en charge les arrêts/redémarrages gracieux, et vous voudrez peut-être utiliser quelque chose comme Docker pour contenir les processus au lieu de les exécuter directement. Il peut être plus sage de déterminer exactement ce dont vous avez besoin pour votre cas spécifique et de proposer un programme d'automatisation pour cela. Ou peut-être utiliser une autre solution beaucoup plus stable et éprouvée disponible partout sur Internet. Mais au cas où vous souhaiteriez déployer quelque chose de très personnalisé, j'espère que cet article vous aidera à le faire et vous montrera combien de temps et d'efforts vous pourriez éventuellement économiser à long terme en automatisant le processus de déploiement de votre application Web.