Go com Docker: Guia Completo 2026

Go e Docker formam uma das duplas mais poderosas do desenvolvimento moderno. Enquanto Go produz binários estáticos e compactos, Docker oferece portabilidade e consistência. O resultado? Imagens de produção com menos de 10MB que rodam em qualquer lugar.

Neste guia, vamos do básico ao avançado: desde o primeiro Dockerfile até um setup de produção completo com Docker Compose, health checks e boas práticas.


Por que Go + Docker Funciona Tão Bem?

Antes de colocar a mão na massa, vale entender por que essa combinação é tão popular:

  1. Binários estáticos: Go compila tudo em um único executável, sem dependências externas
  2. Compilação cruzada nativa: compile para Linux mesmo estando no macOS ou Windows
  3. Imagens minúsculas: com scratch ou Alpine, suas imagens ficam entre 5-15MB
  4. Inicialização instantânea: sem JVM, sem runtime, sem warm-up
  5. Baixo consumo de memória: ideal para containers com limites de recursos

Compare com outras linguagens:

StackTamanho típico da imagem
Go + scratch~5-10MB
Go + Alpine~15-20MB
Node.js + Alpine~150-200MB
Java + JRE~300-500MB
Python + pip~200-400MB

Estrutura do Projeto

Vamos usar este projeto de exemplo ao longo do tutorial:

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

Aqui está nosso código Go inicial:

// cmd/server/main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
)

// Resposta padrão da API
type Resposta struct {
    Mensagem  string `json:"mensagem"`
    Timestamp string `json:"timestamp"`
    Versao    string `json:"versao"`
}

func main() {
    // Porta configurável via variável de ambiente
    porta := os.Getenv("PORT")
    if porta == "" {
        porta = "8080"
    }

    // Versão da aplicação (injetada no build)
    versao := os.Getenv("APP_VERSION")
    if versao == "" {
        versao = "dev"
    }

    mux := http.NewServeMux()

    // Endpoint principal
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        resp := Resposta{
            Mensagem:  "Olá do Go + Docker!",
            Timestamp: time.Now().Format(time.RFC3339),
            Versao:    versao,
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(resp)
    })

    // Health check para o Docker
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, `{"status":"healthy"}`)
    })

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

    log.Printf("Servidor iniciando na porta %s (versão %s)", porta, versao)
    if err := servidor.ListenAndServe(); err != nil {
        log.Fatal("Erro ao iniciar servidor:", err)
    }
}

Dockerfile Básico

Começamos com um Dockerfile simples para entender os fundamentos:

# Dockerfile básico (NÃO use em produção)
FROM golang:1.22

WORKDIR /app

# Copia o código fonte
COPY . .

# Baixa dependências
RUN go mod download

# Compila a aplicação
RUN go build -o server ./cmd/server/

# Expõe a porta
EXPOSE 8080

# Executa o binário
CMD ["./server"]

Problema: essa imagem terá ~800MB porque inclui todo o SDK do Go, ferramentas de compilação, código fonte e cache. Ninguém quer isso em produção.

Construa e teste:

docker build -t minha-api:basico .
docker run -p 8080:8080 minha-api:basico
# Teste: curl http://localhost:8080

Multi-Stage Build: A Forma Certa

Multi-stage builds são o padrão ouro para Go com Docker. Usamos um estágio para compilar e outro, mínimo, para executar:

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

# Instala certificados CA e timezone data
RUN apk add --no-cache ca-certificates tzdata

WORKDIR /build

# Copia apenas os arquivos de dependência primeiro (cache de camadas)
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# Copia o restante do código
COPY . .

# Compila o binário estático
# CGO_ENABLED=0 garante compilação 100% estática
# -ldflags para reduzir tamanho do binário
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags='-w -s -extldflags "-static"' \
    -o server ./cmd/server/

# ===========================================
# Estágio 2: Produção (imagem mínima)
# ===========================================
FROM scratch

# Copia certificados CA (necessário para HTTPS)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copia dados de timezone (necessário para time.LoadLocation)
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Copia o binário compilado
COPY --from=builder /build/server /server

# Porta da aplicação
EXPOSE 8080

# Usuário não-root (segurança)
USER 65534:65534

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD ["/server", "-health"] || exit 1

# Executa o binário
ENTRYPOINT ["/server"]

Resultado: imagem de ~8MB contra ~800MB do Dockerfile básico.

# Construa a imagem otimizada
docker build -t minha-api:prod .

# Verifique o tamanho
docker images minha-api
# REPOSITORY   TAG     SIZE
# minha-api    prod    8.2MB
# minha-api    basico  812MB

Entendendo Cada Flag

FlagPara que serve
CGO_ENABLED=0Desabilita CGo, garante binário 100% estático
GOOS=linuxCompila para Linux (necessário se você está no macOS/Windows)
-ldflags='-w -s'Remove tabela de símbolos e info de debug (reduz ~30% do tamanho)
scratchImagem base vazia, sem sistema operacional

.dockerignore: Não Copie Lixo

O arquivo .dockerignore é tão importante quanto o .gitignore. Sem ele, você envia arquivos desnecessários para o contexto de build:

# Controle de versão
.git
.gitignore

# IDE e editor
.vscode/
.idea/
*.swp
*.swo

# Binários e builds locais
/server
*.exe
*.test

# Docker
Dockerfile
docker-compose.yml
.dockerignore

# Documentação
README.md
docs/

# Testes e CI
.github/
coverage.out
*.prof

# Variáveis de ambiente locais
.env
.env.local

Docker Compose: Go + PostgreSQL

Para desenvolvimento, Docker Compose orquestra múltiplos serviços. Aqui está um setup completo com Go + PostgreSQL + Redis:

# docker-compose.yml
version: "3.9"

services:
  # ========================================
  # Aplicação Go
  # ========================================
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
      - APP_VERSION=1.0.0
      - DATABASE_URL=postgres://usuario:senha123@postgres:5432/minha_api?sslmode=disable
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  # ========================================
  # PostgreSQL
  # ========================================
  postgres:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: usuario
      POSTGRES_PASSWORD: senha123
      POSTGRES_DB: minha_api
    volumes:
      - postgres_dados:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U usuario -d minha_api"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ========================================
  # Redis (cache e sessões)
  # ========================================
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_dados:/data

volumes:
  postgres_dados:
  redis_dados:

Dockerfile para Desenvolvimento com Hot Reload

Para desenvolvimento local, use o Air para hot reload:

# Dockerfile.dev — para desenvolvimento local
FROM golang:1.22-alpine

# Instala o Air para hot reload
RUN go install github.com/air-verse/air@latest

WORKDIR /app

# Copia dependências
COPY go.mod go.sum ./
RUN go mod download

# Copia código fonte
COPY . .

# Porta da aplicação
EXPOSE 8080

# Inicia com hot reload
CMD ["air", "-c", ".air.toml"]

No docker-compose.yml, troque o build para desenvolvimento:

# docker-compose.override.yml (carregado automaticamente)
services:
  api:
    build:
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app           # Monta o código local
      - go_modules:/go   # Cache dos módulos Go

volumes:
  go_modules:

Comandos úteis:

# Subir tudo
docker compose up -d

# Ver logs da API
docker compose logs -f api

# Reconstruir após mudança no Dockerfile
docker compose up -d --build

# Parar tudo e remover volumes
docker compose down -v

Variáveis de Ambiente e Configuração

Go lê variáveis de ambiente nativamente, mas uma abordagem estruturada é melhor:

// internal/config/config.go
package config

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

// Config armazena toda a configuração da aplicação
type Config struct {
    Port         string
    DatabaseURL  string
    RedisURL     string
    JWTSecret    string
    AppVersion   string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

// Carrega lê a configuração das variáveis de ambiente
func Carrega() (*Config, error) {
    cfg := &Config{
        Port:         getEnv("PORT", "8080"),
        DatabaseURL:  getEnv("DATABASE_URL", ""),
        RedisURL:     getEnv("REDIS_URL", ""),
        JWTSecret:    getEnv("JWT_SECRET", ""),
        AppVersion:   getEnv("APP_VERSION", "dev"),
        ReadTimeout:  getDurationEnv("READ_TIMEOUT", 10*time.Second),
        WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 10*time.Second),
    }

    // Validações obrigatórias
    if cfg.DatabaseURL == "" {
        return nil, fmt.Errorf("DATABASE_URL é obrigatória")
    }

    return cfg, nil
}

// getEnv retorna a variável de ambiente ou um valor padrão
func getEnv(chave, padrao string) string {
    if valor, existe := os.LookupEnv(chave); existe {
        return valor
    }
    return padrao
}

// getDurationEnv lê uma duração em segundos da env
func getDurationEnv(chave string, padrao time.Duration) time.Duration {
    if valor, existe := os.LookupEnv(chave); existe {
        if segundos, err := strconv.Atoi(valor); err == nil {
            return time.Duration(segundos) * time.Second
        }
    }
    return padrao
}

Gotchas Comuns: Armadilhas do Go com Docker

1. CGO_ENABLED=0 e a Imagem scratch

Se você esquecer CGO_ENABLED=0, o binário vai depender de bibliotecas C do sistema:

# Erro comum: binário não executa no scratch
standard_init_linux.go: exec user process caused: no such file or directory

Solução: sempre use CGO_ENABLED=0 quando o destino for scratch.

2. Timezone Data Ausente

Sem os dados de timezone, time.LoadLocation("America/Sao_Paulo") retorna erro:

// Isso FALHA no scratch sem timezone data
loc, err := time.LoadLocation("America/Sao_Paulo")
if err != nil {
    log.Fatal("timezone não encontrado:", err)
}

Solução: copie os dados de timezone no Dockerfile (já fizemos acima) ou importe o pacote time/tzdata:

import _ "time/tzdata" // embute os dados de timezone no binário

3. Certificados CA para HTTPS

Sem certificados CA, requisições HTTPS externas falham:

// Isso FALHA no scratch sem certificados CA
resp, err := http.Get("https://api.externa.com/dados")
// x509: certificate signed by unknown authority

Solução: copie os certificados do estágio de build (já fizemos acima).

4. DNS e net Package

O pacote net do Go usa CGo por padrão para resolução DNS. Com CGO_ENABLED=0, usa o resolver puro Go:

// Force o resolver Go puro (recomendado para containers)
// Adicione no início do main():
import "net"

func init() {
    // Usa o resolver DNS puro Go
    net.DefaultResolver.PreferGo = true
}

Build Otimizado para Produção

Aqui está o Dockerfile final com todas as boas práticas reunidas:

# =============================================
# Dockerfile de Produção — Go + Docker
# =============================================

# Argumentos de build
ARG GO_VERSION=1.22
ARG APP_VERSION=dev

# Estágio 1: Dependências (cache eficiente)
FROM golang:${GO_VERSION}-alpine AS deps
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# Estágio 2: Compilação
FROM golang:${GO_VERSION}-alpine AS builder
RUN apk add --no-cache ca-certificates tzdata

WORKDIR /build
COPY --from=deps /go/pkg /go/pkg
COPY . .

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

# Estágio 3: Produção
FROM scratch

# Metadados da imagem
LABEL maintainer="seuprojeto@email.com"
LABEL version="${APP_VERSION}"

# Arquivos essenciais do sistema
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Binário da aplicação
COPY --from=builder /app/server /server

# Porta
EXPOSE 8080

# Segurança: usuário não-root
USER 65534:65534

# Ponto de entrada
ENTRYPOINT ["/server"]

Injetando Versão no Build

No seu main.go, declare a variável que receberá a versão:

// main.go
package main

// Injetado via ldflags durante o build
var version = "dev"

func main() {
    log.Printf("Iniciando servidor v%s", version)
    // ...
}

Build com versão:

docker build \
    --build-arg APP_VERSION=1.2.3 \
    -t minha-api:1.2.3 .

Comandos Docker Essenciais

# Construir imagem
docker build -t minha-api:latest .

# Rodar container
docker run -d -p 8080:8080 --name api minha-api:latest

# Ver logs
docker logs -f api

# Entrar no container (se usar alpine)
docker exec -it api sh

# Inspecionar imagem (ver camadas)
docker history minha-api:latest

# Verificar health check
docker inspect --format='{{.State.Health.Status}}' api

# Remover imagens não utilizadas
docker image prune -a

# Tag e push para registry
docker tag minha-api:latest registry.exemplo.com/minha-api:1.0.0
docker push registry.exemplo.com/minha-api:1.0.0

Conclusão

Go e Docker são uma combinação natural. A capacidade do Go de produzir binários estáticos e auto-contidos permite criar imagens Docker extremamente pequenas e seguras. Recapitulando os pontos principais:

  • Sempre use multi-stage builds para separar compilação e execução
  • Use scratch ou Alpine como imagem base de produção
  • CGO_ENABLED=0 é obrigatório para imagens scratch
  • Copie certificados CA e timezone data quando usar scratch
  • Docker Compose simplifica o desenvolvimento local com múltiplos serviços
  • Health checks garantem que o Docker saiba se sua aplicação está saudável
  • .dockerignore evita enviar arquivos desnecessários no build

Com essas práticas, suas imagens Go ficarão com menos de 10MB, iniciarão em milissegundos e estarão prontas para produção em qualquer ambiente que suporte containers.


Veja também