Production deployment van Go apps: complete gids

Leer hoe je Go applicaties naar productie deployt. Van Docker images en systemd tot CI/CD, zero-downtime releases en health checks.

20 augustus 20267 min leestijdDoor We Develop Communication

Je Go code werkt lokaal perfect, maar dan komt het echte werk: een production deployment die betrouwbaar draait, updates overleeft en niet om drie uur 's nachts je telefoon laat piepen. In deze gids loop je door alle stappen die je nodig hebt om een Go applicatie professioneel live te zetten.

Go leent zich uitstekend voor productie: een enkele statische binary, lage geheugenvoetafdruk en snelle opstarttijden. Maar een solide deployment vraagt meer dan go build. Denk aan Docker images, health checks, graceful shutdown, observability en een CI/CD pipeline die herhaalbaar werkt.

Bouw een productie-binary

Begin bij de build zelf. Een Go binary voor productie compileer je statisch, zonder CGO, voor de target-architectuur van je server.

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
  go build -trimpath -ldflags="-s -w -X main.version=$(git rev-parse --short HEAD)" \
  -o bin/app ./cmd/app

Wat gebeurt hier?

  • CGO_ENABLED=0 zorgt voor een volledig statische binary zonder glibc-afhankelijkheid.
  • -trimpath verwijdert lokale paden uit de binary (reproducibility en security).
  • -ldflags="-s -w" stript debug-informatie en maakt de binary kleiner.
  • -X main.version=... injecteert een versie-string die je in logs en op een /version endpoint kunt tonen.

Check de grootte met ls -lh bin/app. Voor een typische HTTP service kom je vaak onder de 15 MB uit.

Containerize met een multi-stage Dockerfile

Docker is de de-facto standaard voor Go deployments. Een multi-stage build houdt je image minimaal en veilig.

FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -trimpath -ldflags="-s -w" \
    -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

Waarom distroless? Er zit geen shell, geen package manager en geen overbodige libraries in. Minder aanvalsoppervlak en kleinere images. Wil je nog kleiner? Gebruik FROM scratch, maar je mist dan CA-certificaten (die kopieer je apart uit de builder).

Draai altijd als non-root. Dat is geen paranoia, maar basishygiëne voor iedere productie-container.

Configuratie via environment variables

Volg de twelve-factor methodologie: configuratie leest je app uit environment variables. Geen hardcoded waardes, geen config.json die per omgeving verschilt.

type Config struct {
    Port        string
    DatabaseURL string
    LogLevel    string
}

func Load() Config {
    return Config{
        Port:        getenv("PORT", "8080"),
        DatabaseURL: mustGetenv("DATABASE_URL"),
        LogLevel:    getenv("LOG_LEVEL", "info"),
    }
}

Voor complexere setups met config files, validatie en defaults kun je kijken naar libraries als viper. Zie ook onze dependency management gids voor het netjes beheren van zulke externe packages.

Secrets, database wachtwoorden, API keys, JWT secrets, horen in een secret manager (Vault, AWS Secrets Manager, Doppler) of worden door je orchestrator geïnjecteerd. Nooit in Git.

Graceful shutdown is niet optioneel

Bij elke deploy krijgt je oude container een SIGTERM. Als je daar niet op reageert, sluit Docker na 10 seconden hard af, met lopende requests die verdampen.

func main() {
    srv := &http.Server{Addr: ":8080", Handler: router}

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server error: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("graceful shutdown failed: %v", err)
    }
}

Dit patroon hebben we al uitgebreid behandeld in onze gids over HTTP servers bouwen in Go. De kern: nieuwe connecties weigeren, lopende afronden, en pas dan exiten.

Vergeet ook niet je achtergrondwerkers netjes af te sluiten. Als je worker pools en pipelines gebruikt, propageer dan dezelfde context zodat alles tegelijk stopt.

Health checks en readiness

Orchestrators als Kubernetes en load balancers hebben twee soorten checks nodig:

  • Liveness (/healthz): leeft het proces? Zo nee, herstart.
  • Readiness (/readyz): kan de app verkeer verwerken? Zo nee, geen requests sturen.
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
})

http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
    if err := db.PingContext(r.Context()); err != nil {
        http.Error(w, "db unavailable", http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
})

Houd je liveness check simpel en goedkoop. Readiness mag zwaarder zijn: database pings, cache beschikbaarheid, downstream services. Lees ook onze gids over database toegang in Go voor patronen rond connection pools die hierbij relevant zijn.

CI/CD pipeline

Een productie-pipeline bouwt, test en deployt automatisch bij elke push naar je main branch. Een minimale GitHub Actions workflow:

name: deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: '1.23' }
      - run: go test ./...
      - run: go vet ./...
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/org/app:${{ github.sha }}

Essentieel: test en vet moeten slagen vóórdat je image gepusht wordt. Zie onze gids over testing in Go als je daar nog aan werkt.

Tag images met de Git SHA, niet met latest. Zo kun je altijd exact terug naar een eerdere versie.

Zero-downtime releases

Bij een rolling update start je orchestrator nieuwe pods/containers, wacht tot ze ready zijn, en stuurt dan pas verkeer door. De oude containers krijgen SIGTERM zodra de nieuwe gezond zijn.

Drie dingen moeten kloppen:

  1. Readiness probe werkt echt, false positives betekenen downtime.
  2. Graceful shutdown werkt, lopende requests afronden binnen de timeout.
  3. Database migraties zijn backwards-compatible, oude en nieuwe code draaien even naast elkaar.

Dat laatste is het lastigste. Een kolom toevoegen? Prima. Een kolom hernoemen in één deploy? Recept voor een incident. Splits zulke wijzigingen in meerdere releases: eerst toevoegen, dan code migreren, dan oude kolom droppen.

Reverse proxy en TLS

Zet je Go app nooit direct aan het internet. Plaats er een reverse proxy voor, Caddy, Nginx of Traefik, die TLS termineert, rate limiting doet en statische assets serveert.

Caddy is het eenvoudigst: automatisch Let's Encrypt certificaten, één regel config:

app.example.com {
    reverse_proxy localhost:8080
}

Zorg dat je Go app zelf op localhost of een intern netwerk luistert, niet op 0.0.0.0 publiek.

Observability in productie

Een deploy zonder logs en metrics is vliegen zonder instrumentpaneel. Minimaal wil je:

  • Structured logs met log/slog, JSON output die je log aggregator begrijpt.
  • Metrics via Prometheus, request counts, latency, error rates, goroutine count.
  • Tracing via OpenTelemetry, vooral als je meerdere services hebt.

We hebben dit volledig uitgewerkt in de logging en observability gids. Implementeer dit voordat je live gaat, niet erna, debugging zonder instrumentatie is gokken.

Voor microservices is observability extra belangrijk: één trace-ID door alle services maakt het verschil tussen een oplossing in tien minuten of vier uur.

Rollback-strategie

Alles gaat een keer stuk. De vraag is hoe snel je het terugdraait.

  • Houd altijd de vorige image tag beschikbaar in je registry.
  • Zorg dat je kubectl rollout undo of equivalent kunt uitvoeren.
  • Test je rollback procedure minstens één keer in staging. Een rollback die je nog nooit hebt gedraaid is geen rollback, dat is een hoopvol experiment.

Database migraties maken rollback lastig. Vermijd destructive migraties rond risicovolle releases en zorg voor performance monitoring zodat je een regressie snel opmerkt.

Veelgestelde vragen

Wat is de beste manier om een Go applicatie naar productie te deployen?

De meest gebruikte aanpak is een statisch gecompileerde binary in een minimale Docker image (bijvoorbeeld distroless of scratch), draaiend achter een reverse proxy. Voor kleinere setups werkt een systemd service op een Linux server ook uitstekend.

Hoe bouw ik een kleine Docker image voor een Go app?

Gebruik een multi-stage build. In de builder stage compileer je met CGO_ENABLED=0 en kopieer je de binary naar een scratch of distroless image. Zo blijf je vaak onder de 20 MB.

Hoe regel ik zero-downtime deployments bij Go?

Combineer graceful shutdown in je applicatie met een orchestrator die rolling updates ondersteunt, zoals Kubernetes, Nomad of een load balancer met health checks. Nieuwe instances starten eerst en verkeer schakelt pas over na een geslaagde health check.

Welke health checks heb ik nodig?

Minimaal een /healthz (liveness) en /readyz (readiness) endpoint. Liveness controleert of het proces leeft, readiness of de app klaar is voor verkeer, inclusief database- en externe afhankelijkheden.

Hoe ga ik om met secrets en configuratie?

Lees configuratie uit environment variables of een secret manager zoals Vault of AWS Secrets Manager. Commit nooit secrets in je repository en gebruik per-omgeving waardes voor staging en productie.

Conclusie

Een goede production deployment van een Go app is een optelsom van kleine dingen die samen robuust zijn: een statische binary, een minimale container, graceful shutdown, health checks, observability en een pipeline die het allemaal automatiseert. Geen enkel onderdeel is spannend op zichzelf, maar samen zorgen ze dat je met een gerust hart kunt deployen.

Begin klein: containeriseer je app, voeg health checks toe, zet een CI/CD pipeline op. Bouw van daaruit uit naar zero-downtime releases en volledige observability. Zo groeit je setup mee met je project, zonder dat je in één keer alles tegelijk hoeft op te tuigen.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.