CI/CD com Docker e GitHub Actions

    Containerizar a aplicação e usar o Docker como artefato de deploy resolve o problema de "funciona na minha máquina" — a imagem que passa no CI é exatamente a mesma que vai para produção. Este pipeline automatiza build, testes, push para o registry e rollout na VPS.

    Dockerfile otimizado para produção

    Multi-stage build para imagem mínima de produção:

    dockerfile
    # Dockerfile — multi-stage para Node.js:
    FROM node:22-alpine AS base
    WORKDIR /app
    COPY package*.json ./
    
    # Stage de dependências:
    FROM base AS deps
    RUN npm ci
    
    # Stage de build:
    FROM base AS build
    COPY --from=deps /app/node_modules ./node_modules
    COPY . .
    RUN npm run build
    
    # Stage de produção (apenas artefatos finais):
    FROM node:22-alpine AS production
    WORKDIR /app
    ENV NODE_ENV=production
    
    # Instalar apenas dependências de produção:
    COPY package*.json ./
    RUN npm ci --only=production && npm cache clean --force
    
    # Copiar build do stage anterior:
    COPY --from=build /app/dist ./dist
    
    # Criar usuário não-root:
    RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
    USER nodejs
    
    EXPOSE 3000
    HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
      CMD wget -qO- http://localhost:3000/health || exit 1
    
    CMD ["node", "dist/index.js"]

    Pipeline: build, test e push para GHCR

    GitHub Container Registry (GHCR) — gratuito para repositórios públicos e privados:

    yaml
    # .github/workflows/ci.yml
    name: CI — Build e Push Docker
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main]
    
    env:
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}
    
    jobs:
      build-and-push:
        runs-on: ubuntu-latest
        permissions:
          contents: read
          packages: write
    
        steps:
          - uses: actions/checkout@v4
    
          # Cache de layers Docker (acelera builds repetidos):
          - name: Cache Docker layers
            uses: actions/cache@v4
            with:
              path: /tmp/.buildx-cache
              key: ${{ runner.os }}-buildx-${{ github.sha }}
              restore-keys: ${{ runner.os }}-buildx-
    
          - name: Configurar Docker Buildx
            uses: docker/setup-buildx-action@v3
    
          - name: Login no GHCR
            uses: docker/login-action@v3
            with:
              registry: ${{ env.REGISTRY }}
              username: ${{ github.actor }}
              password: ${{ secrets.GITHUB_TOKEN }}
    
          - name: Extrair metadata da imagem
            id: meta
            uses: docker/metadata-action@v5
            with:
              images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
              tags: |
                type=sha,prefix=sha-
                type=ref,event=branch
                type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
    
          - name: Build e push
            uses: docker/build-push-action@v5
            with:
              context: .
              push: ${{ github.event_name != 'pull_request' }}
              tags: ${{ steps.meta.outputs.tags }}
              labels: ${{ steps.meta.outputs.labels }}
              cache-from: type=local,src=/tmp/.buildx-cache
              cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    
          # Rotacionar cache (evita crescimento indefinido):
          - name: Mover cache
            run: |
              rm -rf /tmp/.buildx-cache
              mv /tmp/.buildx-cache-new /tmp/.buildx-cache

    Rodar testes dentro do container no CI

    Garantir que os testes passam na mesma imagem que vai para produção:

    yaml
    # .github/workflows/ci.yml — adicionar job de testes:
      test:
        runs-on: ubuntu-latest
        services:
          # Subir PostgreSQL e Redis para testes de integração:
          postgres:
            image: postgres:16-alpine
            env:
              POSTGRES_DB: testdb
              POSTGRES_USER: test
              POSTGRES_PASSWORD: test
            options: >-
              --health-cmd pg_isready
              --health-interval 10s
              --health-timeout 5s
              --health-retries 5
            ports:
              - 5432:5432
    
          redis:
            image: redis:7-alpine
            options: --health-cmd "redis-cli ping"
            ports:
              - 6379:6379
    
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '22'
              cache: 'npm'
    
          - run: npm ci
    
          - name: Rodar testes
            env:
              DATABASE_URL: postgresql://test:test@localhost:5432/testdb
              REDIS_URL: redis://localhost:6379
              NODE_ENV: test
            run: npm test
    
          - name: Verificar TypeScript
            run: npx tsc --noEmit

    Deploy na VPS com docker-compose pull

    Atualizar o container na VPS após novo push para o registry:

    yaml
    # .github/workflows/deploy.yml — após build e push da imagem:
      deploy:
        needs: [test, build-and-push]
        if: github.ref == 'refs/heads/main'
        runs-on: ubuntu-latest
        steps:
          - name: Deploy via SSH
            uses: appleboy/ssh-action@v1
            with:
              host: ${{ secrets.SSH_HOST }}
              username: ${{ secrets.SSH_USER }}
              key: ${{ secrets.SSH_PRIVATE_KEY }}
              envs: GITHUB_SHA,GITHUB_ACTOR
              script: |
                set -e
                cd /opt/minha-api
    
                # Login no GHCR da VPS:
                echo ${{ secrets.GITHUB_TOKEN }} | \
                  docker login ghcr.io -u ${{ github.actor }} --password-stdin
    
                # Atualizar imagem com o SHA do commit:
                export IMAGE_TAG=sha-${GITHUB_SHA::7}
                sed -i "s|image: ghcr.io/.*/minha-api:.*|image: ghcr.io/org/minha-api:${IMAGE_TAG}|" docker-compose.yml
    
                # Pull da nova imagem e restart:
                docker compose pull api
                docker compose up -d api --no-deps
    
                # Aguardar healthcheck passar:
                sleep 10
                docker compose ps api
    
                # Limpar imagens antigas:
                docker image prune -f

    Variáveis de ambiente e secrets no docker-compose

    Gerenciar configuração de produção no servidor:

    yaml
    # docker-compose.yml — na VPS de produção:
    services:
      api:
        image: ghcr.io/minha-org/minha-api:latest  # tag atualizada pelo CI
        restart: always
        env_file:
          - .env.production  # arquivo gerenciado manualmente no servidor
        environment:
          # Variáveis que podem vir de env do host:
          - GIT_COMMIT=${GIT_COMMIT:-unknown}
        ports:
          - "127.0.0.1:3000:3000"
        healthcheck:
          test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
          interval: 30s
          timeout: 5s
          retries: 3
          start_period: 20s
        logging:
          driver: json-file
          options:
            max-size: "50m"
            max-file: "3"
    
    # .env.production — no servidor, nunca commitado:
    DATABASE_URL=postgresql://user:senha@localhost:5432/producao
    REDIS_URL=redis://localhost:6379
    JWT_SECRET=segredo-muito-longo-aqui
    NODE_ENV=production

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para Docker em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes