`docker compose` com sed -i: por que a config mudou no arquivo mas o container ignorou — e como o inode causou loop de OOM
Publicado em 28 de abril de 2026
O servidor reiniciando por OOM — mas a config estava correta
Um WordPress/Drupal publisher com stack Docker na AWS estava em loop de OOM. O servidor reiniciava repetidamente. Ao conectar via SSH, o uptime mostrava '2 minutes' — tinha acabado de subir do último crash. O site retornava 502.
O servidor era um t4g.medium com 3,4 GB de RAM. Verificação de memória mostrava:
free -h
# total used free shared buff/cache available
# Mem: 3.4Gi 3.0Gi 96Mi 12Mi 370Mi 96Mi
# Swap: 1.2Gi 1.2Gi 0Ki
docker stats --no-stream --format "table {{.Name}} {{.MemUsage}} {{.MemPerc}}"
# CONTAINER MEM USAGE / LIMIT MEM %
# cache 1.796GiB / 3.4GiB 52.38%
# php-fpm 406MiB / 3.4GiB 11.56%
# app-firewall 77MiB / 3.4GiB 2.20%
# security-agent 56MiB / 3.4GiB 1.60%
# nginx 52MiB / 3.4GiB 1.49%
# metrics-exporter 13MiB / 3.4GiB 0.38%Redis (chamado 'cache' no compose) consumindo 1,796 GiB — 52% da RAM total. Com PHP-FPM, nginx e os outros containers somados, o servidor ficava consistentemente acima da capacidade. O kernel OOM killer terminava o processo mais guloso, o container reiniciava, e o ciclo recomeçava.
O fix aplicado na sessão anterior — que não funcionou
Na sessão anterior, a correção havia sido aplicada via `sed -i`:
# Sessão anterior: reduzir maxmemory de 2048mb para 1024mb
sudo sed -i 's/--maxmemory 2048mb/--maxmemory 1024mb/' /opt/myapp/docker-compose.yml
# Verificação imediata mostrava o arquivo correto:
grep maxmemory /opt/myapp/docker-compose.yml
# command: valkey-server --maxmemory 1024mb --maxmemory-policy allkeys-lruO arquivo estava correto. A linha com `1024mb` estava lá. A correção parecia aplicada. Mas o servidor continuou reiniciando por OOM.
Quando o arquivo mostra uma coisa e o container faz outra, o problema geralmente está em qual versão do arquivo o container está lendo — não no conteúdo do arquivo.
O diagnóstico: dois inodes, duas versões do arquivo
A verificação via `docker inspect` revelou o problema:
# Verificar config real do container cache
docker inspect cache --format "{{.Config.Cmd}}"
# [valkey-server --maxmemory 2048mb --maxmemory-policy allkeys-lru]
# ^^^^^^ AINDA 2048mb — ignora o arquivo!
# Verificar o arquivo no host
grep maxmemory /opt/myapp/docker-compose.yml
# command: valkey-server --maxmemory 1024mb --maxmemory-policy allkeys-lru
# ^^^^^^ 1024mb — arquivo corretoO container estava rodando com `--maxmemory 2048mb`. O arquivo tinha `1024mb`. Como podem ser diferentes se o compose é bind-mounted?
O comportamento do sed -i com inodes
O `sed -i` não edita o arquivo in-place. O que ele faz internamente é:
1. Lê o arquivo original (inode A)
2. Cria um arquivo temporário com as substituições (novo inode B)
3. Renomeia o temporário para o nome original
4. O inode A (original) ainda existe se alguém tem o file descriptor aberto
Na prática, após o `sed -i`, o nome `docker-compose.yml` aponta para o inode B (com `1024mb`). Mas o Docker, ao criar o container, abriu um file descriptor para o inode A (com `2048mb`). O container não lê o arquivo pelo nome — ele leu o conteúdo no momento da criação e mantém as configurações.
# Demonstração do comportamento de inodes com sed -i
# Antes:
stat /opt/myapp/docker-compose.yml | grep 'Inode'
# Inode: 1234567
sudo sed -i 's/--maxmemory 2048mb/--maxmemory 1024mb/' /opt/myapp/docker-compose.yml
# Depois:
stat /opt/myapp/docker-compose.yml | grep 'Inode'
# Inode: 1234999 ← NOVO inode! O sed -i criou um arquivo novo
# O container criado com o inode original não enxerga a mudançaIsso se aplica a qualquer configuração passada como argumento no `command:` do compose — o Docker usa o valor no momento da criação do container, não relê o arquivo em cada start. O `nginx -s reload` dentro de um container lê o nginx.conf via bind mount (e por isso funciona com edição in-place usando python3/tee), mas o `--maxmemory` é um argumento de inicialização do Redis, não uma config relida dinamicamente.
Por que nginx -s reload não foi suficiente
Em outro container da mesma stack (WAF com nginx), havia um problema similar: configurações de User-Agent adicionadas via `sed -i` no arquivo `waf.conf` do host não apareciam dentro do container após `nginx -s reload`.
O nginx relê seus arquivos de configuração via bind mount quando recebe SIGHUP (`nginx -s reload`). Mas só funciona se o bind mount ainda aponta para o inode correto. Quando o `sed -i` cria um novo inode para `waf.conf`, o bind mount do Docker continua apontando para o inode antigo — e o nginx relê o arquivo antigo.
# Padrão ERRADO: sed -i + nginx -s reload (não funciona com bind mounts)
sudo sed -i 's/old-value/new-value/' /home/developer/webserver/waf.conf
docker exec app-firewall nginx -s reload # relê o inode antigo — mudança ignorada
# Padrão CORRETO opção 1: python3 preserva o inode (escreve no mesmo arquivo)
python3 -c "
with open('/home/developer/webserver/waf.conf', 'r') as f:
content = f.read()
content = content.replace('old-value', 'new-value')
with open('/home/developer/webserver/waf.conf', 'w') as f:
f.write(content)
"
docker exec app-firewall nginx -s reload # agora funciona — mesmo inode, conteúdo novo
# Padrão CORRETO opção 2: recriar o container
docker compose up -d --force-recreate app-firewallO fix definitivo: recriar o container
A solução para o Redis era simples — recriar o container para que ele lesse o docker-compose.yml atualizado:
cd /opt/myapp && sudo docker compose up -d cache
# Verificação pós-recreate:
docker inspect cache --format "{{.Config.Cmd}}"
# [valkey-server --maxmemory 1024mb --maxmemory-policy allkeys-lru]
# ^^^^^^ agora 1024mb
docker exec cache valkey-cli config get maxmemory
# 1) "maxmemory"
# 2) "1073741824" ← 1 GB exatoCom o container recriado, o Redis passou a usar 1 GB de maxmemory. O resultado em memória foi imediato:
# Comparativo antes/depois da recriação do container cache
# Métrica Antes Depois
# Redis maxmemory 2 GB 1 GB
# Redis uso real 468 MB 458 MB
# RAM livre 370 MB 648 MB
# Disponível ~400 MB 985 MB
# Swap em uso 1.2 GB 88 MB648 MB livres contra 370 MB antes — uma diferença que significa a distância entre OOM crash e servidor estável. O loop de reinicializações parou imediatamente.
A regra geral: sed -i + bind mount = mudança que não persiste
Este é um princípio que se aplica a toda a stack Docker com bind mounts:
Qualquer ferramenta que cria novo arquivo ao editar (sed -i, awk, cp, mv sobre o original) vai criar um novo inode e o bind mount do Docker vai continuar apontando para o inode original. A mudança existe no host mas é invisível para o container.
Ferramentas que preservam o inode (editam in-place):
python3 open(path, 'w') — trunca e reescreve o mesmo inode
tee — escreve no file descriptor existente
perl -pi -e — edita in-place preservando inode (ao contrário do sed -i em alguns sistemas)
vim / nano — dependendo da config, podem preservar ou não o inode
A forma mais segura e explícita de garantir que a mudança chegue ao container:
# Verificar se a mudança está no arquivo do host
grep 'novo-valor' /path/to/config.conf
# Verificar se o container enxerga a mudança
docker exec <container> cat /path/to/config.conf | grep 'novo-valor'
# Se os outputs diferem: recriar o container
docker compose up -d --force-recreate <service>O contexto completo: t4g.medium com Redis 2 GB
A configuração original de `--maxmemory 2048mb` num servidor de 3,4 GB total era insustentável desde o início. Com Redis usando 2 GB, restavam apenas 1,4 GB para todos os outros containers (PHP-FPM, nginx, WAF, CrowdSec) mais o OS. Em condições de tráfego normal, o sistema ficava na borda do OOM; sob tráfego de scrapers, cruzava a linha.
A regra operacional para dimensionar Redis em servidores compartilhados: Redis não deve usar mais de 30-40% da RAM total quando há múltiplos containers. Para um t4g.medium de 3,4 GB, o limite saudável é aproximadamente 1 GB — o que o fix estabeleceu.
O incidente teve duas camadas: a causa imediata (Redis com maxmemory de 2 GB num servidor de 3,4 GB) e a causa oculta (sed -i criando novo inode, fix aparentemente aplicado mas não efetivo). O servidor reiniciava porque o fix nunca chegou ao container. Diagnósticos de OOM que não verificam docker inspect não chegam à causa real.
Lição: sempre verificar docker inspect após mudar config de container
O fluxo correto para qualquer mudança de configuração em containers Docker:
1. Editar o arquivo de configuração no host (usando python3/tee para preservar inode, ou aceitando que precisará recriar)
2. Verificar que o arquivo no host tem o valor correto (grep/cat)
3. Verificar que o container enxerga o valor correto (docker exec + cat, ou docker inspect para args de startup)
4. Se os valores divergem: recriar o container (docker compose up -d <service>)
O passo 3 é o que a maioria das pessoas pula — e é exatamente o que teria evitado o loop de OOM neste caso. O arquivo estava correto. O container não estava.
