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 tagmain. - Ao criar uma tag de versão (por exemplo,
v1.2.0): fazer build de imagens amd64 + arm64, enviar as tags1.2.0,1.2etc. 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.
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 comvou abrir um Pull Request. packages: writepermite que oGITHUB_TOKENfornecido 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 acionamento | Tags geradas |
|---|---|
Push para main | main, sha-1a2b3c4 |
Push da tag v1.2.0 | 1.2.0, 1.2, sha-1a2b3c4 |
| Pull Request #7 | pr-7, sha-1a2b3c4 (constrói, mas não envia) |
type=semversó gera tags quando a tag segue o versionamento semântico (v1.2.0); ovinicial é removido.type=shausa 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âmetro | Descrição |
|---|---|
platforms | Constrói para duas arquiteturas ao mesmo tempo; depois do envio, o resultado é uma imagem multiplataforma |
push | Em Pull Requests, é false e apenas constrói |
build-args | Corresponde a ARG APP_VERSION no Dockerfile, permitindo que o aplicativo exiba sua própria versão |
tags / labels | Usa as saídas do metadata-action da etapa anterior |
cache-from / cache-to: type=gha | Armazena 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 paramainnã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:
| Secret | Conteúdo |
|---|---|
DEPLOY_HOST | IP ou domínio do servidor |
DEPLOY_USER | Nome de usuário SSH |
DEPLOY_SSH_KEY | Chave 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 useonpara definir quando serão executados. - As Actions oficiais do Docker são
setup-qemu,setup-buildx,login,metadataebuild-push. Combinadas, elas permitem construir e enviar imagens para várias plataformas. - Para enviar ao ghcr.io, use o
GITHUB_TOKENfornecido automaticamente e lembre-se de configurarpackages: write. metadata-actiongera automaticamente tags de imagem com base na branch, na tag da versão ou no PR.cache-from/to: type=ghaacelera builds repetidos.- O job de implantação é executado apenas quando há uma tag de versão; por SSH, ele altera o
.enve executapull+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