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 — 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:
# .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-cacheRodar testes dentro do container no CI
Garantir que os testes passam na mesma imagem que vai para produção:
# .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 --noEmitDeploy na VPS com docker-compose pull
Atualizar o container na VPS após novo push para o registry:
# .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 -fVariáveis de ambiente e secrets no docker-compose
Gerenciar configuração de produção no servidor:
# 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.