tva
← Artigos

Uma atualização do Docker Engine em um servidor de produção ativo

Atualizar o Docker Engine em um servidor que está executando ativamente mais de cem contêineres não é uma tarefa que recompensa a improvisação. Quando chegou a hora de migrar do Docker Engine 28.1 para o 28.2 e do Docker Compose 2.35 para o 2.36 no nosso servidor de produção Hetzner, tratamos isso com a mesma disciplina que aplicaríamos a uma migração de banco de dados: uma lista de verificação de preparação por escrito, um caminho de rollback testado, uma sequência de execução precisa e uma fase de verificação pós-atualização antes de declarar a janela fechada.

O que segue é um relato completo de como abordamos isso — o raciocínio por trás de cada etapa, os comandos que realmente executamos e o que observar quando a contagem de contêineres é alta o suficiente para que uma política de reinicialização mal configurada desencadeie uma sequência de falhas que dificulte a recuperação.

Por que esta atualização justificou um procedimento operacional formal

O Docker Engine 28.2 adicionou opções de rede bridge e uma opção do daemon para roteamento direto aos contêineres, e removeu o suporte ao pull de imagens legadas schema 1. O plugin do Compose passou de 2.35.1 para 2.36.0 na mesma janela de manutenção. É uma versão menor, mas vale testar qualquer mudança na inicialização de uma stack, especialmente blocos depends_on com condition: service_healthy.

Em um servidor onde um único docker-compose.yml pode governar doze serviços interdependentes — bancos de dados, proxies reversos, contêineres de aplicação, workers em segundo plano — uma mudança comportamental silenciosa na resolução de dependências é exatamente o tipo de problema que não aparece imediatamente. Ele aparece às 03:00 quando um healthcheck nunca é acionado e um contêiner dependente entra em um ciclo indefinidamente.

A resposta prática é preparação, não cautela por si mesma.

Lista de verificação de preparação

Antes de tocar em um único pacote, trabalhamos com a seguinte lista de verificação. Cada item tem uma razão.

1. Catalogar todos os contêineres em execução e suas políticas de reinicialização.

docker ps --format "table {{.Names}}	{{.Status}}	{{.Image}}" | sort
docker inspect $(docker ps -q) --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}' | sort

Isso fornece uma linha de base. Contêineres com restart: always tentarão subir automaticamente após a reinicialização do daemon Docker — o que é o que você quer, mas somente se os arquivos compose e imagens subjacentes estiverem em um estado conhecido e funcional. Qualquer contêiner em um ciclo de falhas e reinicializações no momento da atualização ainda estará em um ciclo de falhas e reinicializações depois, e é melhor saber disso agora.

2. Validar todos os arquivos Compose antes de atualizar.

docker compose config --quiet 2>&1 | grep -i warning

Execute isso em cada diretório que contém um docker-compose.yml. O comando usa a versão do Compose instalada no momento, portanto revela avisos existentes em vez de comprovar a compatibilidade com a versão de destino; para isso, teste essa versão em um ambiente de homologação. As versões atuais do Compose avisam sobre a chave obsoleta version: no topo dos arquivos Compose (ela é ignorada), e a antiga diretiva links: não é mais necessária, pois serviços em uma rede compartilhada se comunicam pelo nome. Avisos podem se tornar erros em versões posteriores; vale a pena resolvê-los agora.

3. Confirmar espaço em disco disponível.

df -h /var/lib/docker
docker system df

A atualização baixará um novo shim do containerd e substituirá o binário do Docker Engine. O binário antigo e suas dependências nem sempre são limpos automaticamente. Em um servidor em execução há um ano, o docker system df frequentemente revelará vários gigabytes de camadas de imagem recuperáveis. Limpe essas antes da atualização, não durante.

docker system prune -f --volumes

Use --volumes apenas se tiver certeza de que nenhum volume anônimo contém dados que você precisa. Em nosso servidor, todos os dados persistentes são montados a partir de volumes nomeados e declarados explicitamente — então isso era seguro.

4. Exportar a versão atual do Docker Engine e a versão do compose para um arquivo de referência.

docker version > /root/docker-pre-upgrade.txt
docker compose version >> /root/docker-pre-upgrade.txt
docker ps -a >> /root/docker-pre-upgrade.txt
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin >> /root/docker-pre-upgrade.txt

Isso leva dez segundos e fornece uma referência de rollback inequívoca se algo der errado e você precisar identificar quais contêineres estavam em execução no momento da atualização.

5. Notificar os sistemas dependentes.

Nosso servidor executa uma stack Supabase auto-hospedada e várias instâncias de automação de fluxo de trabalho n8n. Qualquer webhook que disparar durante um ciclo de reinicialização de contêiner falhará e, se o remetente não tentar novamente, o evento poderá se perder sem que ninguém perceba. Definimos uma janela de manutenção em nossa ferramenta de monitoramento de disponibilidade e desativamos webhooks de entrada no n8n durante o período.

O plano de rollback

Um plano de rollback só é útil se você tiver decidido antecipadamente qual condição o aciona. O nosso era simples: se algum contêiner que estava em execução antes da atualização não estiver em execução dez minutos após a conclusão da atualização e não puder ser recuperado com um docker compose up -d, revertemos o binário do Docker Engine.

Reverter o Docker Engine em um sistema baseado em Debian significa fixar a versão anterior do pacote. Anotamos a string de versão exata antes de começar:

apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin

Saída em nosso servidor antes da atualização:

docker-ce:
  Installed: 5:28.1.1-1~debian.12~bookworm
  Candidate: 5:28.2.2-1~debian.12~bookworm

docker-compose-plugin:
  Installed: 2.35.1-1~debian.12~bookworm
  Candidate: 2.36.0-1~debian.12~bookworm

O comando de rollback, caso seja necessário:

CONTAINERD_PREV=$(awk '$1 == "containerd.io" {print $2}' /root/docker-pre-upgrade.txt)
apt-get install -y --allow-downgrades   docker-ce=5:28.1.1-1~debian.12~bookworm   docker-ce-cli=5:28.1.1-1~debian.12~bookworm   containerd.io="${CONTAINERD_PREV}"   docker-compose-plugin=2.35.1-1~debian.12~bookworm

O comando também fixa containerd.io na versão salva no arquivo de referência na etapa 4 e passa --allow-downgrades, pois apt-get -y interrompe a operação em vez de instalar versões anteriores dos pacotes de forma não interativa.

Mantivemos este comando em um arquivo de texto no servidor em /root/docker-rollback.sh com permissões de execução, pronto para executar sem digitação sob pressão.

O processo de atualização

A atualização real é simples quando a preparação está completa. A sequência importa: primeiro atualize o índice de pacotes, verifique o que será instalado, depois instale.

Etapa 1: Atualizar o índice apt apenas para o repositório Docker.

apt-get update -o Dir::Etc::sourcelist="sources.list.d/docker.list"   -o Dir::Etc::sourceparts="-"   -o APT::Get::List-Cleanup="0"

Isso restringe a atualização ao repositório Docker em vez de atualizar todas as fontes. Em um servidor de produção, uma atualização de pacote não intencional de uma fonte não relacionada durante uma janela de manutenção do Docker é uma variável que você não precisa.

Etapa 2: Confirmar as versões candidatas.

apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin

Verifique se o candidato corresponde ao que você espera antes de prosseguir. Se o candidato mostrar uma versão mais nova do que o que você testou em staging, pause e avalie.

Etapa 3: Instalar.

apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Isso irá parar o daemon Docker, substituir os binários e reiniciar o daemon. Contêineres com restart: always serão iniciados novamente pelo daemon na reinicialização. A janela em que os contêineres não estão em execução é geralmente inferior a trinta segundos em um servidor de tamanho razoável — embora isso varie dependendo do tempo que o containerd leva para inicializar.

Observe a reinicialização em um segundo terminal:

watch -n 2 'docker ps --format "table {{.Names}}	{{.Status}}" | sort'

Etapa 4: Verificar as versões instaladas.

docker version
docker compose version

Em nosso servidor, a saída confirmou:

Docker Engine: 28.2.2
Docker Compose: v2.36.0

O Docker Compose tem um versionamento independente do Docker Engine: a versão do pacote docker-compose-plugin (aqui, 2.36.0) é a versão do Compose e é o que o índice apt rastreia.

Verificação pós-atualização

Dez minutos após a atualização, fizemos uma verificação estruturada. Isso não é opcional — a reinicialização do daemon iniciará os contêineres, mas não garante que os contêineres que dependem de healthchecks os tenham realmente passado.

Verificar se todos os contêineres estão no estado esperado.

docker ps -a --format "table {{.Names}}	{{.Status}}	{{.RunningFor}}" | sort

Qualquer contêiner mostrando Restarting ou Exited precisa de investigação imediata. Em nosso servidor, um contêiner — um serviço de análise de dados do Supabase — mostrou Restarting (1). Ele estava em um ciclo de falhas e reinicializações de baixa frequência antes da atualização; a atualização não o causou, mas apareceu de forma mais visível na verificação pós-atualização. Este é exatamente o tipo de problema pré-existente que o levantamento pré-atualização existe para capturar.

Verificar se os healthchecks estão passando.

docker inspect $(docker ps -q)   --format '{{.Name}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'   | grep -v "health=none" | sort

Contêineres sem healthchecks explícitos não têm status de integridade; dependendo da versão do Docker, um {{.State.Health.Status}} sem verificação exibe <no value> ou interrompe o comando inteiro com um erro de template. Por isso o template verifica primeiro .State.Health, e o grep descarta os contêineres sem healthcheck. Os que importam são aqueles dos quais seus outros contêineres dependem via condition: service_healthy.

Executar uma verificação de validação de configuração compose em todas as stacks.

for dir in /opt/*/; do
  if [ -f "${dir}docker-compose.yml" ]; then
    echo "--- $dir ---"
    docker compose -f "${dir}docker-compose.yml" config --quiet 2>&1
  fi
done

Isso captura quaisquer diretivas de arquivo compose que a nova versão do Compose trata como avisos ou erros. Em nosso servidor, três stacks emitiram o aviso de obsolescência da chave version. Esses avisos são apenas cosméticos, mas devem ser resolvidos no próximo ciclo de manutenção.

Verificar os logs do daemon Docker para erros.

journalctl -u docker --since "1 hour ago" | grep -i -E "error|warn|fatal"

Uma atualização limpa não produz erros no log do daemon. Avisos sobre configuração obsoleta devem ser registrados, mas não exigem ação imediata.

Confirmar conectividade de rede entre contêineres.

docker network ls
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'

O segundo comando mostra apenas a rede bridge padrão. Repita-o para cada rede personalizada listada por docker network ls para confirmar que as redes bridge personalizadas ainda têm as associações de contêiner esperadas após a reinicialização do daemon, e teste pelo menos uma conexão real entre serviços dependentes.

O que observamos

A atualização foi concluída em menos de dois minutos. Todos os contêineres que estavam em execução antes da atualização estavam em execução depois. A mudança do Compose que notamos mais imediatamente foi um formato de saída --dry-run mais limpo — a nova versão produz saída estruturada e colorida que facilita revisar o que um docker compose up realmente fará antes de executá-lo.

A ordenação de healthcheck depends_on se comportou identicamente ao Compose 2.35 em nossas stacks. Isso era esperado — tínhamos validado os arquivos compose de antemão — mas foi reconfortante confirmar.

Uma constatação importante: o Compose não permite combinar o campo container_name com scale, porque os nomes dos contêineres precisam ser únicos. Tínhamos uma stack que usava ambos — uma herança antiga de antes de passarmos para réplicas adequadas. O Compose emitiu um erro claro no docker compose up para essa stack. A correção foi uma edição de uma linha para remover o campo container_name redundante.

Artigos relacionados

Leitura complementar