Pourquoi Go et Docker sont-ils un duo idéal ?
\n\nGo (ou Golang) est un langage qui, dès sa création, a été conçu pour les architectures cloud modernes. Avant même que Docker ne devienne un standard de facto, Go savait déjà se compiler en un binaire statique ne nécessitant aucune dépendance. Et Docker est une technologie qui permet d'empaqueter une application avec tout son environnement dans un conteneur léger. Lorsque vous combinez ces deux technologies, vous obtenez une solution quasi parfaite : une taille d'image minimale, un démarrage fulgurant et une absence totale de conflits de dépendances.
\n\nDans cet article, nous ne ferons pas de cours théoriques sur la conteneurisation. Au lieu de cela, nous allons écrire une simple application Go, la compiler dans une image Docker, l'optimiser à l'aide d'une construction multi-stage et nous assurer que notre conteneur est prêt pour la production. Tout cela avec des exemples de code réels que vous pouvez exécuter dès maintenant.
\n\nÉtape 1 : Écrire une simple application Go
\n\nPour commencer, créons un simple serveur HTTP en Go. Ce sera notre programme « cobaye ». Créez un fichier main.go avec le contenu suivant :
package main\n\nimport (\n "fmt"\n "log"\n "net/http"\n "os"\n)\n\nfunc main() {\n port := os.Getenv("PORT")\n if port == "" {\n port = "8080"\n }\n\n http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {\n fmt.Fprintf(w, "Bonjour depuis le conteneur Go ! 🐹")\n })\n\n log.Printf("Serveur démarré sur le port %s", port)\n log.Fatal(http.ListenAndServe(":"+port, nil))\n}\n\n\nCe code est un classique : il écoute un port (qui peut être défini via la variable d'environnement PORT) et répond à toute requête par un simple message de bienvenue. Maintenant, nous voulons que cette application fonctionne dans Docker, indépendamment du système d'exploitation sur lequel le conteneur est exécuté.
Étape 2 : Premier Dockerfile — approche naïve
\n\nLa façon la plus simple de « conteneuriser » une application Go est de prendre une image Go complète, d'y copier le code et de l'exécuter. Créez un fichier Dockerfile :
# Utilisation de l'image officielle Go\nFROM golang:1.22\n\n# Définition du répertoire de travail\nWORKDIR /app\n\n# Copie des fichiers du projet\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\n\n# Compilation de l'application\nRUN go build -o main .\n\n# Lancement\nCMD ["./main"]\n\n\nConstruisons l'image et lançons-la :
\n\ndocker build -t go-app:v1 .\ndocker run -p 8080:8080 go-app:v1\n\n\nOuvrez votre navigateur et allez sur http://localhost:8080. Ça marche ? Parfait. Mais il y a un problème : la taille de l'image. Vérifions :
docker images go-app:v1\n\n\nVous verrez quelque chose comme 1.2 GB. C'est une catastrophe ! L'image contient tout un compilateur Go, un gestionnaire de paquets, des utilitaires système — tout ce dont le binaire final n'a pas besoin pour fonctionner. En production, une telle image mettrait une éternité à se charger et prendrait énormément de place.
Étape 3 : Construction multi-stage — le secret des images légères
\n\nDocker prend en charge la construction multi-stage. Cela consiste à utiliser plusieurs instructions FROM dans un seul Dockerfile, et seule la dernière étape est incluse dans l'image finale. La première étape — « de construction » — contient le compilateur et tous les outils. La deuxième étape est minimale, par exemple scratch (image vide) ou alpine.
Réécrivons notre Dockerfile :
\n\n# Étape 1 : Construction\nFROM golang:1.22 AS builder\n\nWORKDIR /app\n\n# Mise en cache des dépendances\nCOPY go.mod go.sum ./\nRUN go mod download\n\n# Copie du code source et compilation\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux go build -o main .\n\n# Étape 2 : Image finale\nFROM alpine:latest\n\nRUN apk --no-cache add ca-certificates\n\nWORKDIR /root/\n\n# Copie uniquement du binaire depuis la première étape\nCOPY --from=builder /app/main .\n\nEXPOSE 8080\n\nCMD ["./main"]\n\n\nQue s'est-il passé ici ? Nous avons créé deux étapes :