Janeiro 2026 · ~8 min

Go com Docker: Multi-Stage Build, Scratch e Compose em 2026

Go com Docker em 2026: multi-stage build, imagem scratch/distroless, CGO_ENABLED=0, docker-compose com Postgres e checklist de produção. Guia prático pt-BR.

Go com Docker: Multi-Stage Build, Scratch e Compose em 2026

Resposta rápida: para Go com Docker em 2026, use multi-stage build (golang:1.24-alpinescratch ou distroless), compile com CGO_ENABLED=0, injete versão via -ldflags, exponha health check HTTP e orquestre dev com docker-compose (API + Postgres). Imagem de produção fica tipicamente em 5–15 MB, sobe em milissegundos e é o padrão que vagas de backend/DevOps pedem no Brasil.

Este guia responde buscas como golang docker, go dockerfile, docker multi-stage go e go docker compose: do primeiro Dockerfile ao setup de produção, com gotchas reais (CA certs, timezone, HEALTHCHECK em scratch, GOMAXPROCS) e links para a trilha de produção do site.

Se você ainda não tem a API, comece por API REST com Go e Go para backend. Para o que roda dentro do container em produção, combine com graceful shutdown, health checks (liveness/readiness) e GOMAXPROCS container-aware.

Por que Go + Docker funciona tão bem?

  1. Binário estático — com CGO_ENABLED=0, um único executável sem runtime externo
  2. Compilação cruzada nativaGOOS=linux no macOS/Windows sem toolchain extra
  3. Imagens minúsculasscratch / distroless na casa de 5–20 MB
  4. Startup instantâneo — sem JVM, sem interpreter, sem warm-up
  5. Baixo consumo de memória — ideal para limites de cgroup em Kubernetes
StackTamanho típico da imagem
Go + scratch~5–10 MB
Go + distroless/static~10–20 MB
Go + Alpine~15–25 MB
Node.js + Alpine~150–200 MB
Java + JRE~300–500 MB
Python + deps~200–400 MB

No mercado brasileiro, “sabe Go e Docker” quase sempre significa: multi-stage, compose com banco, health endpoint e deploy previsível — não só “sabe rodar docker run”.

Estrutura do projeto

minha-api/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   └── handler/
│       └── handler.go
├── go.mod
├── go.sum
├── Dockerfile
├── .dockerignore
└── docker-compose.yml

Código mínimo com health HTTP (o que o orquestrador realmente precisa):

// cmd/server/main.go
package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

// version é injetada no build via -ldflags
var version = "dev"

type Resposta struct {
	Mensagem  string `json:"mensagem"`
	Timestamp string `json:"timestamp"`
	Versao    string `json:"versao"`
}

func main() {
	porta := os.Getenv("PORT")
	if porta == "" {
		porta = "8080"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(Resposta{
			Mensagem:  "Olá do Go + Docker!",
			Timestamp: time.Now().Format(time.RFC3339),
			Versao:    version,
		})
	})
	// Health check HTTP — use este path no Docker/K8s, não um flag inventado
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	})

	srv := &http.Server{
		Addr:         ":" + porta,
		Handler:      mux,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	go func() {
		log.Printf("servidor na porta %s (versão %s)", porta, version)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	// Graceful shutdown: Docker envia SIGTERM no stop
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	<-ctx.Done()

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

Detalhes de sinal, timeout e draining estão em graceful shutdown em Go. Para liveness vs readiness em Kubernetes, use health checks em Go.

Dockerfile básico (só para aprender)

# NÃO use isto em produção — ~800 MB com o SDK inteiro
FROM golang:1.24

WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o server ./cmd/server/
EXPOSE 8080
CMD ["./server"]
docker build -t minha-api:basico .
docker run --rm -p 8080:8080 minha-api:basico
curl -s http://localhost:8080/healthz

O problema: a imagem carrega compilador, cache e source. Em CI e produção isso é lento, caro e aumenta a superfície de ataque.

Multi-stage build: a forma certa

# ===========================================
# Estágio 1: compilação
# ===========================================
FROM golang:1.24-alpine AS builder

RUN apk add --no-cache ca-certificates tzdata git

WORKDIR /build

# Dependências primeiro → cache de camada estável
COPY go.mod go.sum ./
RUN go mod download && go mod verify

COPY . .

ARG APP_VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -trimpath \
    -ldflags="-s -w -X main.version=${APP_VERSION}" \
    -o /out/server ./cmd/server/

# ===========================================
# Estágio 2: produção mínima
# ===========================================
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /out/server /server

EXPOSE 8080
USER 65534:65534
ENTRYPOINT ["/server"]
docker build --build-arg APP_VERSION=1.2.3 -t minha-api:prod .
docker images minha-api
# minha-api  prod    ~8 MB
# minha-api  basico  ~800 MB

Flags que importam

Flag / escolhaPara que serve
CGO_ENABLED=0Binário 100% estático (obrigatório em scratch)
GOOS=linuxTarget Linux mesmo no macOS/Windows
-ldflags="-s -w"Remove símbolos/debug (~20–30% menor)
-X main.version=…Injeta versão no binário
-trimpathPaths reproduzíveis no binário
scratchBase vazia; menor superfície

Alternativa distroless (recomendado em muitos times)

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

Distroless já inclui CA certs em variantes comuns, roda como nonroot e ainda fica pequena. Prefira distroless se o time precisa de um meio-termo entre scratch e Alpine.

HEALTHCHECK: não quebre no scratch

A diretiva HEALTHCHECK do Dockerfile precisa de um binário dentro da imagem. Em scratch não há wget/curl, e um CMD ["/server", "-health"] só funciona se o seu binário implementar essa flag.

Padrões que funcionam:

  1. Orquestrador faz o probe HTTP (Kubernetes httpGet em /healthz, Cloud Run, ECS) — preferível
  2. Alpine/distroless com ferramenta mínima só se você realmente precisa de docker health local
  3. Compose com healthcheck usando a imagem que tem shell, ou probe externo
# docker-compose.yml — probe HTTP via serviço auxiliar ou imagem com curl
healthcheck:
  test: ["CMD", "/server"] # só se o binário tiver subcomando health

Na prática, em produção K8s/Cloud o probe HTTP no path /healthz é o contrato. Não force HEALTHCHECK no scratch se ele não puder rodar.

.dockerignore

.git
.github
.vscode
.idea
*.md
docs/
Dockerfile*
docker-compose*.yml
.dockerignore
.env
.env.*
bin/
dist/
coverage.out
*.test
*.prof

Sem isso, o contexto de build engorda e o cache fica instável.

Docker Compose: Go + PostgreSQL

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        APP_VERSION: "dev"
    ports:
      - "8080:8080"
    environment:
      PORT: "8080"
      DATABASE_URL: postgres://app:app@postgres:5432/minha_api?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: minha_api
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d minha_api"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  pgdata:
docker compose up -d --build
docker compose logs -f api
curl -s http://localhost:8080/healthz
docker compose down -v

Para o lado SQL da aplicação, veja Go com PostgreSQL e database/sql + pool em produção. Configuração via env/Viper: configuração com Viper.

Dev com hot reload (Air)

# Dockerfile.dev
FROM golang:1.24-alpine
RUN go install github.com/air-verse/air@latest
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
EXPOSE 8080
CMD ["air", "-c", ".air.toml"]
# docker-compose.override.yml
services:
  api:
    build:
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - gomod:/go/pkg/mod

volumes:
  gomod:

Produção não monta o source nem usa Air. O artefato de prod é o multi-stage imutável.

Configuração e 12-factor

Containers esperam config por ambiente, não por arquivo bakeado:

porta := os.Getenv("PORT")
dsn := os.Getenv("DATABASE_URL")

Defaults no código ou YAML versionado; segredos e URLs por env/secret do orquestrador. O mesmo binário sobe em dev, staging e prod. Isso é o que conecta Docker a Kubernetes com Go e a releases com GoReleaser (binário + imagem no mesmo pipeline).

Gotchas que quebram deploy

1. Esqueceu CGO_ENABLED=0

standard_init_linux.go: exec user process caused: no such file or directory

Solução: CGO_ENABLED=0 ou use imagem com libc compatível (não scratch).

2. Timezone ausente no scratch

loc, err := time.LoadLocation("America/Sao_Paulo")
// err: unknown time zone America/Sao_Paulo

Solução: copiar /usr/share/zoneinfo ou import _ "time/tzdata".

3. HTTPS externo sem CA certs

x509: certificate signed by unknown authority

Solução: copiar ca-certificates.crt no estágio final (ou distroless).

4. GOMAXPROCS e cgroup

Container com 500m CPU em nó de 16 cores, em Go antigo, usava 16 P. Desde o Go 1.25 o runtime é container-aware. Detalhes: GOMAXPROCS container-aware.

5. Testes de integração sem Docker flaky

Subir Postgres “na mão” no CI quebra. Use Testcontainers em Go para subir dependências reais no teste e derrubar ao final.

6. SIGTERM ignorado

docker stop manda SIGTERM e depois SIGKILL. Sem graceful shutdown, requests em voo morrem no meio do deploy.

Dockerfile de produção (checklist)

ARG GO_VERSION=1.24
ARG APP_VERSION=dev

FROM golang:${GO_VERSION}-alpine AS builder
RUN apk add --no-cache ca-certificates tzdata git
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
ARG APP_VERSION
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -trimpath \
    -ldflags="-s -w -X main.version=${APP_VERSION}" \
    -o /out/server ./cmd/server/

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /out/server /server
EXPOSE 8080
USER 65534:65534
ENTRYPOINT ["/server"]

Checklist rápido antes do push:

  • Multi-stage (builder ≠ runtime)
  • CGO_ENABLED=0 se base for scratch/distroless
  • CA certs + timezone (ou time/tzdata)
  • USER não-root
  • /healthz (ou liveness/readiness separados)
  • Timeouts no http.Server
  • Graceful shutdown em SIGTERM
  • .dockerignore enxuto
  • Versão injetada no binário
  • Secrets só por env/secret manager

Comandos essenciais

docker build --build-arg APP_VERSION=$(git rev-parse --short HEAD) -t minha-api:latest .
docker run --rm -p 8080:8080 -e PORT=8080 minha-api:latest
docker compose up -d --build
docker compose logs -f api
docker history minha-api:latest
docker image prune -f

Trilha de produção (próximos passos)

ObjetivoPágina
API HTTP completaAPI REST com Go
Backend no BrasilGo para backend
BancoGo com PostgreSQL
Shutdown limpoGraceful shutdown
Probes K8sHealth checks
CPU em cgroupGOMAXPROCS
Testes com containersTestcontainers
Release + imagemGoReleaser
OrquestraçãoGo no Kubernetes
VagasVagas DevOps · Vagas backend

Conclusão

Go com Docker em 2026 não é “colocar o SDK na imagem”. É multi-stage, binário estático, base mínima, health HTTP, config por env e shutdown educado. Com isso você entrega imagens de menos de 15 MB, deploys previsíveis e um portfólio alinhado ao que fintechs e times de plataforma pedem no Brasil.

Próximo passo prático: containerize a API do tutorial de API REST, suba Postgres no compose e adicione testes com Testcontainers. Depois, publique binário e imagem com GoReleaser.

Perguntas frequentes

Como fazer Dockerfile multi-stage para Go?

Use um estágio builder com golang:*-alpine para compilar com CGO_ENABLED=0 e um estágio final mínimo (scratch, distroless ou alpine). Copie só o binário, certificados CA e timezone data. O resultado típico fica entre 5 e 20 MB, em vez de ~800 MB de uma imagem com o SDK completo.

Qual a melhor imagem base para Go em produção?

scratch ou gcr.io/distroless/static-debian12 são as escolhas mais comuns em 2026. scratch é a menor; distroless facilita debugging e já traz CA certs. Alpine serve quando você precisa de shell ou de ferramentas no container. Evite golang:latest em produção — ela carrega o compilador inteiro.

Por que CGO_ENABLED=0 é obrigatório no Docker com scratch?

Sem CGO_ENABLED=0 o binário liga dinamicamente a libc. Na imagem scratch não há libc, e o container falha com 'no such file or directory'. CGO_ENABLED=0 gera binário estático e roda em scratch/distroless. Só ligue CGO quando precisar de dependências C (sqlite com cgo, algumas libs de crypto).

Como usar docker-compose com Go e PostgreSQL?

Defina serviços api e postgres, passe DATABASE_URL por environment, use depends_on com condition: service_healthy e healthcheck no Postgres (pg_isready). Em dev, monte o código com volume e use Air para hot reload. Em prod, construa multi-stage e não monte o source.

Go precisa de Docker para deploy?

Não. Go gera binário único e pode rodar em VM, bare metal ou systemd. Docker (e Kubernetes) viraram o padrão de entrega porque padronizam env, rede, health check e escala. Se a vaga pede Go + Docker, o multi-stage build e o compose com banco são o mínimo esperado no portfólio.

Como o Go se comporta em containers (GOMAXPROCS)?

Desde o Go 1.25 o runtime fica container-aware: GOMAXPROCS respeita o cgroup CPU limit sem gambiarra. Em versões anteriores, um container com 0,5 CPU em host de 16 cores usava 16 threads de runtime. Veja o guia de GOMAXPROCS container-aware no blog.