Uma Atualização Importante 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 para o 29 e do Docker Compose v2 para o v5 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 cause uma cascata em uma recuperação difícil.
Por Que Esta Atualização Justificou um Runbook Formal
O Docker Engine 29 introduziu mudanças no caminho de integração do containerd e ajustou como o docker compose lida com a resolução de dependências em blocos depends_on com condition: service_healthy. O Docker Compose v5, enquanto isso, é uma reescrita interna significativa — passando do binário v2 baseado em Go para uma nova arquitetura que resolve vários problemas de longa data com a ordenação de inicialização paralela, mas também deprecia um punhado de diretivas de arquivo Compose que o v2 ignorava silenciosamente.
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 loop 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. Inventariar 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 bom conhecido. Qualquer contêiner em um crash-loop no momento da atualização ainda estará em um crash-loop depois, e é melhor saber disso agora.
2. Validar todos os arquivos compose contra o schema v5 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 Compose v5 é mais rigoroso sobre chaves depreciadas — particularmente version: no topo dos arquivos compose (agora ignorado, mas gera um aviso), e qualquer uso da diretiva links: depreciada. Avisos no v5 podem se tornar erros em versões subsequentes; 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
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 sistemas downstream.
Nosso servidor executa uma stack Supabase em self-hosting 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á silenciosamente no lado do remetente. Definimos uma janela de manutenção em nossa ferramenta de monitoramento de uptime 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:29.0.1-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:
apt-get install -y docker-ce=5:28.1.1-1~debian.12~bookworm docker-ce-cli=5:28.1.1-1~debian.12~bookworm containerd.io docker-compose-plugin=2.35.1-1~debian.12~bookworm
Mantivemos este comando em um arquivo de texto no servidor em /root/docker-rollback.sh com permissões de execução, pronto para rodar 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: 29.0.1
Docker Compose: v2.36.0
Observe que o Docker Compose v5 é distribuído como versão 2.36.x do pacote docker-compose-plugin — a designação "v5" refere-se à versão de reescrita interna, não ao semver do pacote. Isso causa alguma confusão na documentação; a versão do pacote é 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 analytics do Supabase — mostrou Restarting (1). Ele estava em um crash-loop de baixa frequência antes da atualização também; 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 inventário pré-atualização existe para capturar.
Verificar se os healthchecks estão passando.
docker inspect $(docker ps -q) --format '{{.Name}} health={{.State.Health.Status}}' 2>/dev/null | grep -v "health=<no value>" | sort
Contêineres sem healthchecks explícitos mostrarão <no value> — filtre-os. 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 o Compose v5 agora trata como avisos ou erros. Em nosso servidor, três stacks emitiram o aviso de depreciação da chave version. Esses são cosméticos, mas valem 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 depreciada valem ser anotados, mas não agir imediatamente.
Confirmar conectividade de rede entre contêineres.
docker network ls
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'
O Docker Engine 29 não alterou o comportamento padrão do driver de rede, mas vale confirmar que as redes bridge personalizadas ainda têm as associações de contêiner esperadas após a reinicialização do daemon.
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 Docker Compose v5 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 v2 em nossas stacks. Isso era esperado — tínhamos validado os arquivos compose de antemão — mas foi reconfortante confirmar.
Uma mudança substantiva: o Compose v5 não ignora mais silenciosamente o campo container_name em combinação com scale. Tínhamos uma stack que usava ambos — uma herança antiga de antes de passarmos para réplicas adequadas. O Compose v5 emitiu um erro claro no docker compose up para essa stack, onde o v2 havia silenciosamente ignorado o conflito. A correção foi uma edição de uma linha para remover o campo container_name redundante.
Insights Relacionados
- Implantando Aplicações React em Produção: Configuração Completa com Docker e Traefik como Proxy Reverso
- Construindo uma Stack de Desenvolvimento Multi-Tenant com Docker
- Traefik como Proxy Reverso: O Guia Completo de Self-Hosting para HTTPS e Automação de SSL
- Self-Hosting do n8n no Hetzner Cloud: Tutorial Completo de Configuração com Docker