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-alpine → scratch 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?
- Binário estático — com
CGO_ENABLED=0, um único executável sem runtime externo - Compilação cruzada nativa —
GOOS=linuxno macOS/Windows sem toolchain extra - Imagens minúsculas —
scratch/ distroless na casa de 5–20 MB - Startup instantâneo — sem JVM, sem interpreter, sem warm-up
- Baixo consumo de memória — ideal para limites de cgroup em Kubernetes
| Stack | Tamanho 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 / escolha | Para que serve |
|---|---|
CGO_ENABLED=0 | Binário 100% estático (obrigatório em scratch) |
GOOS=linux | Target Linux mesmo no macOS/Windows |
-ldflags="-s -w" | Remove símbolos/debug (~20–30% menor) |
-X main.version=… | Injeta versão no binário |
-trimpath | Paths reproduzíveis no binário |
scratch | Base 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:
- Orquestrador faz o probe HTTP (Kubernetes
httpGetem/healthz, Cloud Run, ECS) — preferível - Alpine/distroless com ferramenta mínima só se você realmente precisa de
dockerhealth local - Compose com
healthcheckusando 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=0se base for scratch/distroless - CA certs + timezone (ou
time/tzdata) -
USERnão-root -
/healthz(ou liveness/readiness separados) - Timeouts no
http.Server - Graceful shutdown em SIGTERM
-
.dockerignoreenxuto - 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)
| Objetivo | Página |
|---|---|
| API HTTP completa | API REST com Go |
| Backend no Brasil | Go para backend |
| Banco | Go com PostgreSQL |
| Shutdown limpo | Graceful shutdown |
| Probes K8s | Health checks |
| CPU em cgroup | GOMAXPROCS |
| Testes com containers | Testcontainers |
| Release + imagem | GoReleaser |
| Orquestração | Go no Kubernetes |
| Vagas | Vagas 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.