Mais de Cem Contêineres Docker: Nossa Rotina Mensal de Verificação de Saúde
Rodar 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 é fazer o deploy desses contêineres — é mantê-los.
A maioria da documentação cobre como fazer os contêineres rodarem. 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 é o modo de falha agudo mais comum 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. Rodar 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 pendentes e cache de build de seis meses de deploys iterativos 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 combinadas — acumulados silenciosamente a partir de meses de builds acionados por CI que nunca foram limpos.
Mas na realidade, o número mais importante é a coluna de reclaimable. Antes de tocar em qualquer coisa, estabelecemos uma linha de base: quanto espaço está atualmente disponível para reclamação e qual é a trajetória mês a mês. Se o espaço reclaimable está crescendo, o agendamento de limpeza precisa se tornar mais agressivo ou mais frequente.
Nossa sequência de limpeza roda nesta ordem. Primeiro, volumes não mais vinculados a nenhum contêiner:
docker volume prune -f
Em seguida, imagens pendentes — 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 um rebuild completo é 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 pendentes. Rodamos isso somente após verificar que todos os serviços podem ser reconstruídos a partir do registro 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 deploy 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, incluindo na reinicialização do daemon, independentemente do código de saída. 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 stateless 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 reiniciaria independentemente de por que parou.
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 rodar indefinidamente.
Nossa verificação mensal roda 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 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 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 verificaçã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 stateful 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 folha expirado: navegadores e clientes podem relatar erros confusos em vez da clara mensagem “certificado expirado” que a maioria dos engenheiros espera.
Mantemos um shell script 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 roda como um cron job, mas também o rodamos manualmente durante a revisão mensal como confirmação secundária de que a saída do cron foi precisa. Cron jobs 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 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}} {{.MemLimit}}"
Contêineres mostrando 0B / 0B na coluna de limite de memória não têm restrição definida. Revisamos cada um e determinamos um limite apropriado. Para serviços HTTP stateless, um limite de memória de duas a quatro vezes o working set observado é um bom ponto de partida. O objetivo não é ser preciso — é evitar crescimento ilimitado que derrube serviços co-localizados.
Também olhamos para a porcentagem de CPU durante a passagem de estatísticas. Um contêiner consistentemente perto de cem por cento de CPU em um host multicore sugere um processo descontrolado ou um contêiner sub-provisionado 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 agendamentos irregulares. Um contêiner rodando uma imagem que era atual seis meses atrás pode estar rodando contra uma versão do nginx ou PostgreSQL com vulnerabilidades conhecidas. Não fazemos pull e redeploy automaticamente de cada contêiner todo mês — isso cria mais risco do que mitiga. Mas verificamos o que está rodando 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 changelog upstream uma vez por mês. Para serviços usando uma tag flutuante como latest ou 16-alpine, fazemos pull e comparamos o digest da imagem para determinar se algo mudou. Se mudou, revisamos o que mudou antes do deploy.
Mas na realidade, a disciplina mais importante é afastar-se de tags flutuantes. Um serviço que fez redeploy 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 rodando uma imagem antiga conhecida. Fixe versões, então atualize-as deliberadamente.
A Disciplina
Tomada em conjunto, 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. Nenhuma dessas tarefas requer mais de noventa minutos no total em um servidor bem documentado. O valor não está nas verificações individuais — está em realizá-las em um agendamento fixo, antes que algo quebre e não depois.
Sistemas de produção degradam gradualmente. O disco acumula. 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 move o ônus da manutenção de reativo para previsível, que é uma postura operacional completamente diferente.
Insights 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
Artigos relacionados
Quando a Autenticação Docker Quebra o Login Mobile: Uma História de Bug Cross-Platform
Implantando Aplicações React em Produção: Configuração Completa com Docker e Proxy Reverso Traefik
Construindo uma Stack de Desenvolvimento Multi-Tenant com Docker: Configuração Completa para Implantações Escaláveis de Clientes