Conteúdo

Tutorial de CI/CD com GitHub Actions: faça build e envie imagens Docker automaticamente

Em Implantar em um host na nuvem, o build, o envio e a atualização no servidor são feitos manualmente. Nesta seção, você vai automatizar essas etapas com o GitHub Actions:

  • A cada push para main: fazer build e enviar a imagem automaticamente, com a tag main.
  • Ao criar uma tag de versão (por exemplo, v1.2.0): fazer build de imagens amd64 + arm64, enviar as tags 1.2.0, 1.2 etc. e, em seguida, atualizar o servidor automaticamente por SSH.
  • Pull Request: fazer apenas o build, sem enviar a imagem, para confirmar que o Dockerfile está correto.
Fluxo do git push ao build no GitHub Actions, envio para ghcr.io e atualização do servidor
Fluxo de CI/CD: push do código → build no GitHub Actions → ghcr.io → servidor baixa a nova versão

Estrutura do projeto

Vamos usar o projeto Node.js de Implantar em um host na nuvem e adicionar um arquivo de workflow:

myapp/
├── .github/
│   └── workflows/
│       └── docker.yml
├── Dockerfile
├── package.json
├── package-lock.json
└── server.js

O GitHub executa automaticamente todos os arquivos YAML em .github/workflows/.

Workflow completo

.github/workflows/docker.yml:

name: Build and push image

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Check Dockerfile
        run: docker build --check .

      - uses: docker/setup-qemu-action@v4

      - uses: docker/setup-buildx-action@v4

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Decide image tags
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          build-args: APP_VERSION=${{ github.ref_name }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - name: Update the server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /srv/myapp
            VERSION='${{ github.ref_name }}'
            sed -i "s/^APP_VERSION=.*/APP_VERSION=${VERSION#v}/" .env
            docker compose pull app
            docker compose up -d app

Explicação por etapas

Gatilhos e permissões

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:

permissions:
  contents: read
  packages: write
  • Executa ao enviar para a branch main, enviar uma tag começando com v ou abrir um Pull Request.
  • packages: write permite que o GITHUB_TOKEN fornecido automaticamente envie imagens ao ghcr.io. É uma boa prática conceder apenas as permissões necessárias.

Verificar antes do build

      - name: Check Dockerfile
        run: docker build --check .

O ambiente de execução ubuntu-latest do GitHub já vem com Docker instalado. docker build --check verifica apenas a sintaxe do Dockerfile (veja a introdução em docker init); avisos não fazem o build falhar, mas aparecem nos registros. Este é o resultado da verificação local do Dockerfile deste projeto:

$ docker build --check . 2>&1 | tail -n 1
Check complete, no warnings found.

QEMU e Buildx

      - uses: docker/setup-qemu-action@v4
      - uses: docker/setup-buildx-action@v4

O ambiente de execução do GitHub é amd64. Para fazer build de imagens arm64 quando o Dockerfile contém etapas que executam programas, como RUN npm ci, é necessário emular com QEMU (veja o motivo em Imagens multiplataforma). setup-buildx-action cria um builder compatível com várias plataformas e com a exportação de cache.

Fazer login no Registry

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

Pull Requests não enviam imagens, então não é necessário fazer login (PRs externos também não têm permissão para enviar imagens). Você não precisa criar secrets.GITHUB_TOKEN por conta própria.

Para enviar ao Docker Hub, primeiro crie um Access Token no Docker Hub e salve-o em Settings → Secrets and variables → Actions do repositório no GitHub. Depois:

        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

(Sem registry, o destino é o Docker Hub.) Altere também images do metadata-action para sua-conta/myapp.

Determinar tags da imagem automaticamente

docker/metadata-action gera tags com base no evento que acionou o workflow:

Evento de acionamentoTags geradas
Push para mainmain, sha-1a2b3c4
Push da tag v1.2.01.2.0, 1.2, sha-1a2b3c4
Pull Request #7pr-7, sha-1a2b3c4 (constrói, mas não envia)
  • type=semver só gera tags quando a tag segue o versionamento semântico (v1.2.0); o v inicial é removido.
  • type=sha usa os primeiros 7 caracteres do commit, facilitando identificar a versão do código usada para construir a imagem.
  • Ele também gera labels no padrão OCI (repositório de origem, commit e horário de criação), que o build-push-action grava na imagem.

Construir e enviar

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          build-args: APP_VERSION=${{ github.ref_name }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
ParâmetroDescrição
platformsConstrói para duas arquiteturas ao mesmo tempo; depois do envio, o resultado é uma imagem multiplataforma
pushEm Pull Requests, é false e apenas constrói
build-argsCorresponde a ARG APP_VERSION no Dockerfile, permitindo que o aplicativo exiba sua própria versão
tags / labelsUsa as saídas do metadata-action da etapa anterior
cache-from / cache-to: type=ghaArmazena o cache de build no espaço de cache do GitHub Actions. mode=max também armazena em cache as camadas das etapas intermediárias. Na próxima vez, se você alterar apenas o código, a camada de npm ci poderá ser reutilizada diretamente

Implantação automática

  deploy:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
  • needs: build: é executado somente se o build for bem-sucedido.
  • if: implanta apenas quando uma tag de versão é enviada. Um push para main não altera o ambiente de produção.

appleboy/ssh-action faz login no servidor por SSH e executa o script. O conteúdo corresponde às etapas feitas manualmente em Implantar em um host na nuvem: alterar APP_VERSION em .env, depois executar pull e up -d. ${{ github.ref_name }} é o nome da tag (v1.2.0), e ${VERSION#v} remove o v inicial. O efeito desta linha de sed foi verificado localmente:

$ VERSION='v1.2.0'; sed -i "s/^APP_VERSION=.*/APP_VERSION=${VERSION#v}/" .env; cat .env
APP_VERSION=1.2.0
DB_PASSWORD=secret

Configure estes três valores em Secrets no repositório do GitHub:

SecretConteúdo
DEPLOY_HOSTIP ou domínio do servidor
DEPLOY_USERNome de usuário SSH
DEPLOY_SSH_KEYChave privada SSH dedicada à implantação (a chave pública correspondente deve estar em ~/.ssh/authorized_keys no servidor)

Recomendamos criar um usuário e uma chave exclusivos para a implantação. Não use a chave com que você faz login no servidor.

Validar o workflow com actionlint

Quando há um erro no workflow, você só o descobre depois de enviá-lo ao GitHub, e corrigir e enviar repetidamente leva tempo. A ferramenta de código aberto actionlint verifica a sintaxe e as expressões localmente. Você pode executá-la em um contêiner sem instalá-la:

$ docker run --rm -v "$(pwd):/repo" -w /repo rhysd/actionlint:latest -color=false -verbose .github/workflows/docker.yml
verbose: Linting .github/workflows/docker.yml
verbose: Found 0 parse errors in 1 ms for .github/workflows/docker.yml
verbose: Found total 0 errors in 13 ms for .github/workflows/docker.yml

0 errors significa que não há problemas. Vamos escrever github.ref_name incorretamente de propósito, como github.ref_nam:

$ docker run --rm -v "$(pwd):/repo" -w /repo rhysd/actionlint:latest -color=false .github/workflows/docker.yml
.github/workflows/docker.yml:52:39: property "ref_nam" is not defined in object type {action: string; action_path: string; action_ref: string; action_repository: string; action_status: string; actor: string; actor_id: string; api_url: string; artifact_cache_size_limit: number; base_ref: string; env: string; event: object; event_name: string; event_path: string; graphql_url: string; head_ref: string; job: string; output: string; path: string; ref: string; ref_name: string; ref_protected: bool; ref_type: string; repository: string; repository_id: string; repository_owner: string; repository_owner_id: string; repository_visibility: string; repositoryurl: string; retention_days: number; run_attempt: string; run_id: string; run_number: string; secret_source: string; server_url: string; sha: string; state: string; step_summary: string; token: string; triggering_actor: string; workflow: string; workflow_ref: string; workflow_sha: string; workspace: string} [expression]
   |
52 |           build-args: APP_VERSION=${{ github.ref_nam }}
   |                                       ^~~~~~~~~~~~~~

actionlint aponta que ref_nam não existe e lista todas as propriedades disponíveis no objeto github. O GitHub pode não informar esse tipo de erro; o valor simplesmente se torna uma string vazia.

O workflow deste capítulo foi validado com actionlint, mas não foi realmente enviado ao GitHub para execução (isso criaria um pacote em ghcr.io e faria uma conexão SSH com um servidor real). Na primeira execução no seu repositório, confira os registros de cada etapa na página Actions.

Configuração no servidor

No compose.yaml do servidor, altere a imagem para o endereço do ghcr.io:

  app:
    image: ghcr.io/yourname/myapp:${APP_VERSION}

Os pacotes recém-criados no ghcr.io são privados por padrão. Primeiro, faça login no servidor (com um Personal Access Token que tenha a permissão read:packages):

echo "seu token" | docker login ghcr.io -u yourname --password-stdin

--password-stdin lê a senha da entrada padrão, sem deixá-la no histórico de comandos.

Processo para publicar uma nova versão

Depois de concluir a configuração, basta fazer o seguinte para publicar uma nova versão:

git tag v1.2.0
git push origin v1.2.0

O GitHub Actions vai, em sequência: verificar o Dockerfile → construir para amd64 e arm64 → enviar ghcr.io/yourname/myapp:1.2.0 e 1.2 → conectar-se ao servidor por SSH e alterar APP_VERSION para 1.2.0 → baixar a imagem e recriar o contêiner app.

Para reverter, altere APP_VERSION para a versão anterior no servidor e execute docker compose up -d app. A imagem antiga continua disponível no ghcr.io.

Resumo

  • Coloque os workflows em .github/workflows/ e use on para definir quando serão executados.
  • As Actions oficiais do Docker são setup-qemu, setup-buildx, login, metadata e build-push. Combinadas, elas permitem construir e enviar imagens para várias plataformas.
  • Para enviar ao ghcr.io, use o GITHUB_TOKEN fornecido automaticamente e lembre-se de configurar packages: write.
  • metadata-action gera automaticamente tags de imagem com base na branch, na tag da versão ou no PR.
  • cache-from/to: type=gha acelera builds repetidos.
  • O job de implantação é executado apenas quando há uma tag de versão; por SSH, ele altera o .env e executa pull + up -d.
  • Use o actionlint para verificar o workflow localmente antes.

Exercícios

Exercício 1: Altere o workflow para que, ao enviar para main, ele construa apenas para amd64 (mais rápido), e só construa para duas plataformas quando houver uma tag de versão.

Resposta

platforms também pode usar uma expressão:

          platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}

Depois de alterar, verifique uma vez com o actionlint.

Exercício 2: O que acontece se você criar uma tag release-1 (que não começa com v)?

Resposta

Nada acontece. on.push.tags corresponde apenas a v*; release-1 não aciona o workflow. A condição if do job deploy também aceita apenas tags que começam com refs/tags/v.

Exercício 3: Use o actionlint para verificar os arquivos de workflow do seu próprio projeto e veja se há algum erro que passou despercebido.

Perguntas frequentes

O GitHub Actions é pago?

Usar os ambientes de execução fornecidos pelo GitHub em repositórios públicos (public) é grátis; repositórios privados têm uma cota grátis mensal e só há cobrança quando ela é excedida. Consulte a documentação oficial do GitHub para ver os limites e preços.

Por que não preciso criar uma senha separada para enviar imagens ao ghcr.io?

A cada execução do workflow, o GitHub fornece automaticamente um GITHUB_TOKEN temporário. Basta adicionar packages: write a permissions no workflow para usá-lo e enviar imagens ao ghcr.io da mesma conta ou organização.

O servidor exibe unauthorized ao baixar imagens do ghcr.io. O que fazer?

Por padrão, os pacotes ghcr.io recém-criados são privados. No servidor, primeiro execute docker login ghcr.io com um Personal Access Token que tenha a permissão read:packages, ou torne o pacote público nas configurações de pacotes do GitHub.

O build multiplataforma está muito lento. O que fazer?

No ambiente de execução x86 do GitHub, o arm64 é emulado com QEMU, e as etapas que precisam compilar ficam muito mais lentas. Você pode fazer build de arm64 apenas ao criar uma tag de versão, usar cache-from: type=gha ou mudar para ambientes de execução arm64 para fazer builds separados. Para linguagens que permitem compilação cruzada (Go, Rust), consulte as instruções em [Imagens multiplataforma](/pt/docker/multi-platform/).

Atualizado 13 de outubro de 2026