Mais de cem contêineres Docker: nossa rotina mensal de verificação de saúde
Executar mais de cem contêineres Docker em produção não é incomum se você foi construindo serviços gradualmente ao longo de vários anos. Uma stack Supabase auto-hospedada por si só representa treze contêineres. Adicione um frontend web, múltiplos serviços de API, workers em segundo plano, uma stack de monitoramento, agregação de logs, e o número cresce mais rapidamente do que a maioria dos planos antecipa. O que se torna difícil não é implantar esses contêineres — é mantê-los.
A maioria da documentação cobre como colocar os contêineres em execução. Muito pouco cobre o que acontece seis meses depois quando o disco enche em um domingo, ou quando você descobre que um terço dos seus contêineres não tem política de reinicialização, ou quando um certificado SSL expirou silenciosamente porque o alerta de monitoramento estava silenciado. Esta publicação documenta nossa rotina mensal para um servidor de produção com mais de cem contêineres.
Começando com o disco
O disco é a causa mais comum de falhas súbitas em ambientes Docker de longa duração. O Docker acumula dados de formas que não são visíveis para ferramentas padrão do sistema. Executar df -h mostra o uso do sistema de arquivos, mas não informa que o Docker está retendo cinquenta gigabytes de camadas de contêineres parados, imagens sem referência (dangling) e cache de build de seis meses de implantações sucessivas que nunca foram limpos.
O ponto de partida correto é docker system df, que divide o uso de disco por imagens, contêineres, volumes locais e cache de build. A saída costuma ser surpreendente. Já vimos servidores onde o cache de build sozinho excedia todas as camadas de contêineres em execução somadas — esse cache se acumulou silenciosamente a partir de meses de builds acionados por CI que nunca foram limpos.
Mas na realidade, o número mais importante é a coluna reclaimable. Antes de tocar em qualquer coisa, estabelecemos uma linha de base: quanto espaço está atualmente disponível para recuperação e qual é a trajetória mês a mês. Se o espaço recuperável está crescendo, o agendamento de limpeza precisa se tornar mais agressivo ou mais frequente.
Nossa sequência de limpeza é executada nesta ordem. Primeiro, volumes anônimos que não são mais referenciados por nenhum contêiner. Desde o Docker 23.0, docker volume prune mantém os volumes nomeados não utilizados, a menos que você adicione --all; antes de adicionar essa opção, verifique se nenhum volume desvinculado contém dados de que você ainda precisa:
docker volume prune -f
Em seguida, imagens sem referência (dangling) — camadas sem tag que não são referenciadas por nenhum contêiner em execução ou parado:
docker image prune -f
E por fim, se confirmamos que uma reconstrução completa é viável dentro da nossa janela de recuperação, todas as imagens não utilizadas com mais de sete dias:
docker image prune -a --filter "until=168h" -f
O sinalizador -a remove todas as imagens não utilizadas, não apenas as sem referência. Executamos esse comando somente após verificar que todos os serviços podem ser reconstruídos a partir do registro de imagens dentro do nosso tempo de recuperação aceitável. Essa verificação acontece antes do comando, não depois.
Políticas de reinicialização
As políticas de reinicialização determinam o que acontece quando um contêiner sai inesperadamente ou quando o daemon Docker reinicia após uma reinicialização do host. A maioria dos guias de implantação menciona isso brevemente. Mas na realidade, uma política de reinicialização configurada incorretamente é como você acaba com serviços que ficaram parados silenciosamente por duas semanas — e ninguém notou porque o alerta de monitoramento estava apontado para o endpoint errado.
O Docker fornece quatro políticas de reinicialização. no é o padrão: o contêiner não reinicia sob nenhuma circunstância. always reinicia o contêiner sempre que ele para, independentemente do código de saída; após um docker stop manual, ele só volta quando o daemon é reiniciado ou o contêiner é iniciado novamente. unless-stopped se comporta como always, mas respeita paradas explícitas — se você executar docker stop antes de uma reinicialização, o contêiner permanece parado após a reinicialização. on-failure[:max-retries] reinicia somente em códigos de saída diferentes de zero, com um limite opcional de novas tentativas antes de desistir.
Para serviços web sem estado e workers de API, usamos unless-stopped. Se pararmos deliberadamente um contêiner durante uma janela de manutenção, ele deve permanecer parado após a próxima reinicialização em vez de voltar inesperadamente. always o iniciaria novamente na próxima reinicialização do daemon, mesmo que o tivéssemos parado deliberadamente.
Para contêineres de migração de banco de dados ou trabalhos de inicialização únicos, a política correta é no. Uma migração que falha não deve entrar em loop. on-failure:3 é apropriado para contêineres que devem tentar novamente brevemente contra uma dependência que pode estar temporariamente indisponível — um consumidor de fila externo aguardando um broker ficar acessível, por exemplo — mas não deve ser executado indefinidamente.
Nossa verificação mensal executa um único comando em todos os contêineres:
docker inspect --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' $(docker ps -aq)
Qualquer contêiner com política no que não seja um trabalho único intencional é revisado. Na maioria dos casos, significa que um serviço foi iniciado com docker run durante um incidente e nunca foi formalmente adicionado à configuração do Docker Compose com uma política de reinicialização adequada.
Rotação de logs
O driver de log padrão do Docker é json-file. Por padrão, ele não impõe limites de tamanho. Um contêiner emitindo um fluxo modesto de linhas de log pode produzir centenas de gigabytes ao longo de vários meses. Isso não é uma preocupação teórica — é uma das causas mais comuns de esgotamento de disco em servidores de produção que foram configurados sem gerenciamento deliberado de logs.
A correção é uma política global em /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}
Isso limita os logs de cada contêiner a cinco arquivos de cem megabytes cada — quinhentos megabytes no máximo por contêiner. O daemon Docker deve ser reiniciado após essa mudança e, criticamente, os contêineres devem ser recriados, não apenas reiniciados, para que as novas configurações de log entrem em vigor.
Mas na realidade, a configuração do daemon.json se aplica apenas a contêineres criados após a mudança. Contêineres existentes retêm sua configuração de log original indefinidamente. Esse é o erro mais comum que encontramos: a política é definida, o daemon é reiniciado e a suposição é de que todos os contêineres agora estão em conformidade. Não estão. Nossa revisão mensal verifica a configuração de log por contêiner:
docker inspect --format '{{.Name}} {{.HostConfig.LogConfig}}' $(docker ps -q)
Contêineres sem limites de tamanho explícitos são recriados com a configuração atualizada durante a próxima janela de manutenção. A ordem de recriação importa — serviços com estado precisam que seus volumes de dados permaneçam no lugar, e serviços dependentes precisam iniciar na sequência correta.
Expiração de certificados SSL
Certificados SSL expiram. O monitoramento automatizado detecta a maioria dos casos. Mas o monitoramento automatizado também fica mal configurado, produz fadiga de alertas ou falha silenciosamente junto com o serviço que deve monitorar. Nossa rotina mensal inclui uma verificação manual independente de qualquer sistema automatizado.
Para cada domínio voltado ao público, verificamos o certificado diretamente:
echo | openssl s_client -connect domain.com:443 -servername domain.com 2>/dev/null | openssl x509 -noout -enddate
Isso gera a data notAfter. Qualquer coisa expirando dentro de trinta dias entra na fila de renovação imediatamente, independentemente do que qualquer painel de monitoramento diga. A verificação manual é a salvaguarda.
Para infraestrutura de certificados autogerenciada — que operamos para vários serviços internos — verificamos os certificados intermediários separadamente dos certificados folha. Um intermediário expirado causa falha de validação da cadeia completa mesmo quando o certificado folha em si ainda é válido. Esse modo de falha é menos visível do que um certificado folha expirado: navegadores e clientes podem relatar erros confusos em vez da clara mensagem “certificate expired” que a maioria dos engenheiros espera.
Mantemos um script de shell que itera sobre uma lista de domínios, extrai a data de expiração via openssl e imprime um aviso para qualquer coisa dentro de trinta dias e um alerta crítico para qualquer coisa dentro de sete dias. Esse script é executado como um tarefa agendada no cron, mas também o executamos manualmente durante a revisão mensal como confirmação secundária de que a saída do cron estava correta. Tarefas agendadas no cron falham silenciosamente com mais frequência do que a maioria das pessoas espera.
Limites de recursos de contêineres
Sem limites de memória, um contêiner com mau comportamento pode esgotar a RAM do host e acionar o OOM killer do kernel em processos não relacionados. Sem limites de CPU, um processo descontrolado pode privar contêineres vizinhos de recursos por tempo suficiente para causar falhas em cascata. Nenhum desses é um caso extremo raro.
A revisão mensal verifica os limites de recursos em todos os contêineres em execução:
docker stats --no-stream --format "table {{.Name}} {{.CPUPerc}} {{.MemUsage}}"
A coluna de memória mostra o uso e o limite juntos; um contêiner sem limite de memória mostra a memória total do host como limite, enquanto 0B / 0B aparece apenas para contêineres que não estão em execução. Revisamos cada um e determinamos um limite apropriado. Para serviços HTTP sem estado, um limite de memória de duas a quatro vezes a memória efetivamente usada durante a operação é um bom ponto de partida. O objetivo não é ser preciso — é evitar crescimento ilimitado que derrube serviços no mesmo host.
Também olhamos para a porcentagem de CPU durante a coleta de estatísticas. Um contêiner consistentemente perto de cem por cento de CPU (no docker stats, 100% equivale a um núcleo inteiro, portanto valores maiores são possíveis em um host multicore) sugere um processo descontrolado ou um contêiner com recursos insuficientes para sua carga de trabalho. Ambas as condições justificam investigação antes do próximo mês.
Verificações de atualização de imagens
As imagens base recebem patches de segurança em intervalos irregulares. Um contêiner executando uma imagem que era atual seis meses atrás pode estar usando uma versão do nginx ou PostgreSQL com vulnerabilidades conhecidas. Não baixamos novas imagens nem reimplantamos cada contêiner automaticamente todo mês — isso cria mais risco do que mitiga. Mas verificamos o que está em execução em relação ao que é atual.
A abordagem prática: para cada serviço com uma versão de imagem fixada, verificamos a versão fixada em relação ao histórico de alterações do projeto de origem uma vez por mês. Para serviços usando uma tag flutuante como latest ou 16-alpine, baixamos a imagem e comparamos seu hash identificador (digest) para determinar se algo mudou. Se mudou, revisamos o que mudou antes da implantação.
Mas na realidade, a disciplina mais importante é afastar-se de tags flutuantes. Um serviço que foi reimplantado silenciosamente com uma mudança incompatível porque sua tag latest apontava para uma nova versão principal é um problema mais difícil de diagnosticar do que um serviço executando uma imagem antiga conhecida. Fixe as versões e atualize-as deliberadamente.
A disciplina
Considerada como um todo, a verificação mensal de saúde cobre seis áreas: uso de disco e limpeza, verificação de política de reinicialização, configurações de rotação de log, expiração de certificados SSL, limites de recursos de contêineres e atualidade das imagens. O conjunto dessas tarefas leva no máximo noventa minutos em um servidor bem documentado. O valor não está nas verificações individuais — está em realizá-las em uma periodicidade fixa, antes que algo quebre e não depois.
Sistemas de produção degradam gradualmente. O uso de disco aumenta. Os logs crescem. Os certificados envelhecem. As imagens ficam desatualizadas. Nenhum desses processos gera um alerta até que o limiar seja cruzado. A verificação mensal torna a manutenção previsível em vez de reativa, que é uma postura operacional completamente diferente.
Artigos relacionados
- Construindo um Data Warehouse da Amazon com FastAPI e TimescaleDB — a mesma disciplina de infraestrutura aplicada à camada de dados
- Operações Solo em Escala: Gerenciando Dezenas de Projetos com uma Equipe Pequena — como estruturamos a manutenção recorrente em um grande portfólio de projetos