Retour au cours

backend / go

Déploiement : build cross-platform et Docker

Leçon 261 exercice

Explication

Ce que vous allez apprendre

  • Cross-compiler un binaire Go pour une autre plateforme sans toolchain externe
  • Réduire la taille d'un binaire et y injecter des informations de version
  • Construire une image Docker minimale avec un build multi-stage
  • Comprendre pourquoi CGO_ENABLED=0 est nécessaire pour une image scratch
  • Orchestrer un service Go avec docker-compose et un healthcheck

Dans quel contexte ?

Une équipe développe sur des Mac Apple Silicon (darwin/arm64) mais doit déployer son service sur des serveurs Linux amd64 en production. Avec la plupart des langages compilés, cette différence de plateforme demanderait une machine ou une VM dédiée pour compiler le bon binaire. Avec Go, une seule commande suffit, exécutée directement depuis le Mac de développement, sans aucun outil supplémentaire à installer.

D'abord, la cross-compilation est native au toolchain Go

GOOS=linux GOARCH=amd64 go build suffit pour produire un binaire Linux 64 bits depuis n'importe quelle machine de développement, macOS ou Windows inclus. Ce n'est possible que parce que Go embarque son propre runtime et ne dépend d'aucune bibliothèque système externe par défaut — contrairement à des langages qui nécessitent un compilateur natif de la plateforme cible.

Une fois le binaire produit, deux ajustements améliorent le déploiement

-ldflags="-s -w" retire les symboles de débogage du binaire final, réduisant sensiblement sa taille sans changer son comportement. -ldflags="-X main.Version=1.2.3" permet d'injecter des informations comme le numéro de version ou le hash du commit Git directement au moment de la compilation, sans avoir besoin d'un fichier de configuration séparé à maintenir.

TechniqueEffetBénéfice
GOOS/GOARCHCible une autre plateformeCompiler pour Linux depuis macOS/Windows
-ldflags="-s -w"Retire les symboles de debugBinaire plus léger
CGO_ENABLED=0Désactive les appels CBinaire 100% statique, compatible scratch
Build Docker multi-stageSépare compilation et exécutionImage finale de quelques Mo au lieu de plusieurs centaines

Prérequis

Cette leçon suppose une connaissance de base de Docker (images, conteneurs) — voir les cours dédiés Docker de la plateforme pour les fondamentaux si besoin.

Il reste une variable d'environnement cruciale pour obtenir un binaire vraiment portable

Par défaut, certains packages de la bibliothèque standard (notamment la résolution DNS) peuvent utiliser CGO, une passerelle vers des bibliothèques C du système. CGO_ENABLED=0 désactive totalement cette dépendance, produisant un binaire 100% statique qui n'a besoin d'absolument aucune bibliothèque système pour s'exécuter.

Piège fréquent

Oublier CGO_ENABLED=0 avant de copier un binaire Go dans une image Docker FROM scratch (qui ne contient aucune bibliothèque système) provoque une erreur au démarrage du conteneur, du type "exec format error" ou un plantage lié à une bibliothèque introuvable — un piège classique et frustrant lors des premiers déploiements Docker en Go.

Enfin, le build multi-stage sépare deux préoccupations distinctes

La première étape (FROM golang:1.23-alpine AS builder) contient tout l'outillage de compilation Go, potentiellement plusieurs centaines de Mo. La seconde étape (FROM scratch) ne copie que le binaire compilé et les certificats TLS nécessaires, produisant une image finale minuscule sans aucune surface d'attaque inutile (pas de shell, pas de gestionnaire de paquets).

Bonne pratique

Ajoute systématiquement un healthcheck dans ton docker-compose.yml ou ta configuration Kubernetes : un service qui démarre sans erreur n'est pas nécessairement un service prêt à recevoir du trafic, et un healthcheck permet à l'orchestrateur de ne router du trafic qu'une fois le service réellement opérationnel.

Ce cours t'a mené de ton tout premier go run jusqu'à un service Go conteneurisé, testé, profilé et prêt pour la production — les bases sont maintenant solides pour continuer à explorer l'écosystème Go en autonomie, que ce soit avec des frameworks comme Gin ou Echo, ou des bases de données via database/sql.

Commandes & code

Déploiement : build cross-platform et Docker

Go compile en binaire statique unique : c'est un atout majeur pour le déploiement.

bash
# Cross-compilation native : GOOS/GOARCH sans toolchain externe
GOOS=linux GOARCH=amd64 go build -o app-linux-amd64 main.go
GOOS=darwin GOARCH=arm64 go build -o app-mac-arm64 main.go
GOOS=windows GOARCH=amd64 go build -o app.exe main.go

# Reduire la taille du binaire (retire les symboles de debug)
go build -ldflags="-s -w" -o app main.go

# Injecter des informations de version a la compilation (sans fichier de config)
go build -ldflags="-X main.Version=1.2.3 -X main.Commit=$(git rev-parse HEAD)" -o app main.go
go
// fichier: main.go
package main

import "fmt"

// Variables remplies via -ldflags -X au moment du build
var (
	Version = "dev"
	Commit  = "inconnu"
)

func main() {
	fmt.Printf("app version=%s commit=%s\n", Version, Commit)
}
dockerfile
# Multi-stage build : image finale minuscule (quelques Mo, sans toolchain Go)
FROM golang:1.23-alpine AS builder

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download

COPY . .
# CGO_ENABLED=0 : binaire 100% statique, compatible avec une image "scratch"
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app ./cmd/api

# Image finale ultra-legere, sans shell ni package manager (surface d'attaque minimale)
FROM scratch

# Certificats TLS necessaires pour les appels HTTPS sortants
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app /app

EXPOSE 8080
ENTRYPOINT ["/app"]
yaml
# docker-compose.yml : service + healthcheck pour orchestration locale/CI
services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    healthcheck:
      test: ["CMD", "/app", "-healthcheck"]
      interval: 10s
      timeout: 3s
      retries: 3
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_PASSWORD=pass
bash
# Build et publication
docker build -t monorg/monapi:1.2.3 .
docker run -p 8080:8080 monorg/monapi:1.2.3

# Verifier la taille de l'image (souvent < 20 Mo pour un service Go typique)
docker images monorg/monapi

Résumé

  • GOOS/GOARCH permettent de cross-compiler sans toolchain tierce ni machine cible.
  • CGO_ENABLED=0 produit un binaire statique compatible avec une image scratch ou distroless.
  • Un build multi-stage sépare la compilation (image Go complète) de l'exécution (image minimale).
  • -ldflags -X injecte version/commit sans fichier de configuration additionnel.

Exercices pratiques

1 disponible
1

Mission : une image Docker qui plante avec exec format error

Objectif : Corriger un build Docker multi-stage cassé par un CGO oublié, et cross-compiler correctement un binaire pour la cible de production.

Contexte

L'équipe développe sur des Mac Apple Silicon et doit déployer sur des serveurs Linux amd64 via une image Docker FROM scratch. Le Dockerfile actuel construit avec RUN GOOS=linux go build -o /app ./cmd/api (sans toucher à CGO_ENABLED). Le build Docker réussit sans erreur, mais le conteneur plante immédiatement au démarrage en production.

Résoudre l’exercice →