Bem-vindo ao Blog da DMarkInfo

Conteúdos e novidades sobre Tecnologia da Informação.

Automatizando Migrations em Go com Docker e CI/CD

Postado por Eduardo Marques em 01/11/2025
Automatizando Migrations em Go com Docker e CI/CD

Quando comecei a trabalhar com aplicações em Go que dependiam de bancos de dados relacionais, uma das maiores dores que encontrei foi manter as migrations organizadas e executadas de forma confiável entre os ambientes — desenvolvimento, staging e produção.
Se você já passou por aquele cenário em que o código está na nova versão, mas o banco não acompanhou, sabe exatamente o tipo de problema que estou falando.

Foi aí que eu decidi automatizar o processo de migrations usando Docker e integrar tudo ao meu pipeline de CI/CD. Neste artigo, vou mostrar como fiz isso do zero, os desafios que encontrei e as boas práticas que adotei para deixar o processo limpo, previsível e totalmente automatizado.

 

Por que automatizar migrations?

Em um projeto real, o banco de dados é tão parte da aplicação quanto o próprio código. De nada adianta fazer deploy de uma nova versão se as alterações no esquema do banco não forem aplicadas junto.
Automatizar migrations traz uma série de benefícios:

  • Evita divergências entre ambientes (dev, staging e produção);

  • Garante que toda alteração no schema esteja versionada;

  • Permite rollback em caso de erro;

  • Dá rastreabilidade e previsibilidade nos deploys;

  • E, o mais importante: elimina o “e se eu esqueci de rodar a migration antes do deploy?”.

 

Ferramentas que uso

No ecossistema Go existem várias opções, mas a combinação que escolhi e recomendo é:

  • golang-migrate → ferramenta consolidada, suporta vários bancos e pode rodar via CLI ou dentro do código Go.

  • Docker → para empacotar tanto o app quanto o processo de migração.

  • GitHub Actions (ou qualquer CI/CD que você use) → para automatizar a execução das migrations a cada deploy.

myapp/
├── cmd/
│   └── api/
│       └── main.go
├── database/
│   └── migrations/
│       ├── 000001_init.up.sql
│       ├── 000001_init.down.sql
│       ├── 000002_add_users.up.sql
│       └── 000002_add_users.down.sql
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── go.sum

O diretório database/migrations guarda os arquivos versionados de migração.
Eu sigo o padrão 000001_descricao.up.sql e 000001_descricao.down.sql — o up aplica e o down reverte.

 

Criando migrations

Com o golang-migrate instalado, criar uma nova migration é simples:

migrate create -ext sql -dir database/migrations -seq add_users_table

Isso gera:

000003_add_users_table.up.sql
000003_add_users_table.down.sql

No arquivo .up.sql, adiciono as alterações:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

E no .down.sql, o rollback:

DROP TABLE IF EXISTS users;

 

Rodando migrations localmente

Para testar, basta apontar o comando para o banco local (no meu caso, PostgreSQL):

migrate -path database/migrations -database "postgres://user:pass@localhost:5432/mydb?sslmode=disable" up

E se eu quiser reverter a última alteração:

migrate -path database/migrations -database "..." down 1

 

Automatizando com Docker

Depois de escrever migrations e testar manualmente, vem o pulo do gato: automatizar com Docker.

Meu Dockerfile ficou assim:

# Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp ./cmd/api

# Final
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
COPY database/migrations ./migrations

# Instala o CLI migrate
RUN apk add --no-cache curl \
    && curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz \
         | tar xvz \
    && mv migrate.linux-amd64 /usr/local/bin/migrate \
    && chmod +x /usr/local/bin/migrate

ENV DATABASE_URL="postgres://user:pass@db:5432/mydb?sslmode=disable"

# Ao iniciar o container, roda as migrations e depois sobe o app
ENTRYPOINT ["sh", "-c", "migrate -path=/root/migrations -database \"$DATABASE_URL\" up && ./myapp"]

Com esse setup, toda vez que o container sobe, ele verifica e aplica automaticamente as migrations pendentes antes de rodar o binário Go.

 

docker-compose para desenvolvimento

O docker-compose.yml que uso localmente ficou assim:

version: "3.8"
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  api:
    build: .
    depends_on:
      - db
    environment:
      DATABASE_URL: "postgres://user:pass@db:5432/mydb?sslmode=disable"
    ports:
      - "8080:8080"

volumes:
  pgdata:

Ao rodar:

docker-compose up --build

o banco inicia, o container da API executa automaticamente as migrations e em seguida sobe o app.

Simples e funcional.

 

Integrando com CI/CD

Agora vem a cereja do bolo: integrar o processo no pipeline.

A ideia é que a cada novo deploy, o pipeline execute as migrations antes de atualizar a aplicação.
No meu caso, uso GitHub Actions, mas o conceito serve para qualquer CI.

O workflow (.github/workflows/ci-cd.yml) ficou assim:

name: Go CI/CD

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: 1.21
      - name: Run tests
        run: go test ./... -v

  deploy:
    needs: build
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
    steps:
      - uses: actions/checkout@v3
      - name: Build and push image
        run: |
          docker build -t myrepo/myapp:${{ github.sha }} .
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push myrepo/myapp:${{ github.sha }}

      - name: Run migrations
        run: |
          docker run --rm \
            -v $(pwd)/database/migrations:/migrations \
            migrate/migrate \
            -path=/migrations \
            -database "$DATABASE_URL" up

      - name: Deploy application
        run: |
          # exemplo: kubectl set image deployment/myapp myapp=myrepo/myapp:${{ github.sha }}

Dessa forma, o pipeline:

  1. Roda os testes;

  2. Gera a imagem Docker e envia para o registry;

  3. Executa as migrations no banco remoto;

  4. Só então atualiza o ambiente de produção.

Se a migração falhar, o deploy é automaticamente interrompido.

 

Boas práticas que aprendi no processo

  • Nunca execute migrations dentro da etapa de docker build — o banco não está disponível nesse momento.

  • Sempre teste as migrations em um ambiente staging antes de ir para produção.

  • Mantenha o diretório migrations/ versionado no Git.

  • Faça migrations pequenas, frequentes e reversíveis.

  • E, acima de tudo: nunca confie em migrations manuais no deploy.

Automatizar migrations foi um divisor de águas na minha rotina de deploys em Go.
Hoje, o fluxo é previsível, padronizado e sem surpresas: o pipeline cuida de aplicar o que precisa, na hora certa, com rastreabilidade total.

Se você ainda está rodando migrations “na mão” ou copiando SQL no terminal, recomendo fortemente investir nessa automação.
Acredite: depois que você configura uma vez, não volta mais atrás.

Compartilhe este post:
Voltar para a Home