Limites de memória no Docker Compose sem OOM
Defina limites de memória e CPU no Docker Compose para evitar que um contentor derrube a VPS. Entenda deploy.resources, mem_limit, exit 137, swap e dimensionamento.
O que faz um limite de memória do Docker Compose
Um limite de memória do Docker Compose é um teto rígido que o kernel Linux aplica ao cgroup (grupo de controlo, a funcionalidade do kernel que contabiliza os recursos de um conjunto de processos) de um contentor. Defina deploy.resources.limits.memory num serviço e esse contentor nunca poderá usar mais do que o valor especificado. Quando tenta ultrapassar esse valor, o kernel termina um processo dentro do contentor, que normalmente sai com o código 137.
Isto é mais importante numa VPS, onde a RAM é fixa e não existe memória livre do host para utilizar. Um contentor com uma fuga de memória ou uma consulta problemática pode consumir todas as páginas livres de um servidor com 8GB. O kernel termina então o processo que considera mais problemático, muitas vezes uma base de dados ou a sua sessão SSH, em vez do contentor que causou o problema. Os limites transformam uma indisponibilidade de todo o servidor num problema isolado de um serviço, que pode ser reiniciado.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mAplique a configuração e confirme que o limite está ativo:
docker compose up -d
docker stats --no-streamA coluna MEM USAGE / LIMIT deve apresentar algo semelhante a 142MiB / 1GiB. Se a coluna do limite mostrar toda a RAM do host, a configuração não foi aplicada e o restante deste guia não será útil até que isso seja corrigido. Se o ficheiro Compose for novo para si, os conceitos básicos do Docker Compose para uma VPS explicam a estrutura de ficheiros usada aqui.
deploy.resources.limits ou mem_limit: qual deles se aplica
Existem duas formas de escrever a mesma ideia, o que causa esta confusão.
mem_limit, mem_reservation, memswap_limit, cpus e cpu_shares são chaves de serviço de nível superior herdadas dos formatos antigos dos ficheiros Compose. deploy.resources veio do schema do Swarm e agora faz parte da Compose Specification, que é o formato que docker compose lê atualmente.
Ambos funcionam num único host. Docker Compose, o plugin docker compose, aplica deploy.resources.limits e deploy.resources.reservations quando executa docker compose up, sem qualquer cluster Swarm. Atualmente, o plugin está na linha v5 (v5.6.0 era a versão estável mais recente no momento da verificação, em 2026-10-10). Passou diretamente da v2 para a v5, saltando as versões 3 e 4 para evitar confusão com as versões antigas do formato dos ficheiros Compose. Nenhuma versão v5 alterou a forma como os limites de recursos são tratados. As partes orientadas para Swarm do bloco deploy são as outras chaves, como placement, update_config e endpoint_mode, que descrevem o agendamento e os rollouts num cluster de nós para docker stack deploy. Portanto, a recomendação comum de que "deploy requer Swarm" está errada para a subseção de recursos. Segui-la deixa os seus serviços sem qualquer limite.
Escolha uma forma de escrita por projeto. Escrever mem_limit: 512m e deploy.resources.limits.memory: 1g no mesmo serviço é mais do que difícil de ler: a Compose Specification indica que, quando mem_limit está definido, ele deve ser consistente com limits.memory, mas não indica qual valor prevalece quando são diferentes. Independentemente da forma escolhida, confirme o que o daemon aplicou efetivamente:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Os valores de memória são expressos em bytes, por isso 1g apresenta o valor como 1073741824. A CPU é expressa em nano CPUs, por isso 1.5 apresenta o valor como 1500000000. Um 0 em qualquer campo significa que não foi definido nenhum limite. O menor limite de memória aceite pelo Docker é 6m. Abaixo desse valor, o container não arranca.
O que acontece quando um contentor atinge o limite
O contentor não fica mais lento. Termina.
Quando um processo solicita uma página e o cgroup já atingiu o seu memory.max, o kernel começa por recuperar o que conseguir dentro desse cgroup: a cache de páginas limpas e, depois, as páginas que puder trocar para swap. Se essa recuperação não libertar memória suficiente, o OOM killer do cgroup escolhe um processo dentro do contentor e envia-lhe SIGKILL. Terminar o PID 1 do contentor termina o contentor. O código de saída 137 é simplesmente 128 mais o sinal 9. Portanto, 137 é a impressão digital de qualquer SIGKILL, não uma prova de OOM por si só.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 é um OOM kill. false 137 normalmente significa que outra coisa enviou SIGKILL. Um programa também pode terminar com 137 de propósito, mas isso é raro. A causa habitual é docker compose stop atingir o seu período de tolerância de dez segundos porque a aplicação ignorou SIGTERM. Esta distinção poupa horas, porque os dois problemas não têm qualquer relação.
Há mais dois locais que registam o evento. Monitorize o daemon em tempo real:
docker events --filter event=oomDepois, leia o log do kernel, que é o registo que sobrevive a um reinício:
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Um kill de cgroup escreve uma linha que começa por Memory cgroup out of memory: Killed process 24713 (node). Essa linha significa que um contentor atingiu o seu próprio limite. Pode aparecer enquanto o host ainda tem bastante RAM livre. Uma linha sem o prefixo Memory cgroup indica um OOM do host. Isso significa que a própria máquina ficou sem RAM. É precisamente a falha que os limites devem evitar. Se aparecer, normalmente significa que a soma dos seus limites é demasiado elevada, que alguns serviços não têm qualquer limite ou que algo fora do Docker está a usar mais memória do que a margem que deixou.
Com restart: unless-stopped, um ciclo de OOM pode passar despercebido, porque o serviço aparece como ativo em docker compose ps um segundo depois de terminar. Verifique a coluna de tempo de atividade e o contador de reinícios. Associe o limite a um healthcheck que indique que a aplicação está não saudável para tornar visível um contentor que termina repetidamente, sem precisar de o monitorizar.
Reserva é uma indicação; o limite é a regra
reservations.memory (o antigo mem_reservation) é um patamar mínimo flexível. O Docker descreve-o como um limite flexível que é ativado quando o daemon deteta contenção ou pouca memória no host. Nunca impede um contentor de ultrapassá-lo e nunca garante que a memória estará livre quando o contentor a solicitar. Apenas orienta o kernel a recuperar primeiro memória dos contentores que estejam acima da respetiva reserva.
Por si só, uma reserva não protege nada. Use-a para indicar um serviço que deve ser tratado de forma favorável sob pressão e dependa do limite para garantir a segurança. Mantenha a reserva abaixo do limite; caso contrário, o contentor não arranca: o Docker rejeita a configuração com Minimum memory limit can not be less than memory reservation limit.
Contabilidade de swap, sem rodeios
A maioria das imagens de VPS não inclui qualquer ficheiro de swap. Execute swapon --show e free -h. Se o total de swap for zero, todas as definições relacionadas com swap abaixo não têm efeito, e o seu limite de memória é apenas um limite de RAM.
memswap_limit não representa a quantidade de swap. Representa o total de memória mais swap. Com mem_limit: 1g e memswap_limit: 2g, o contentor recebe 1GB de RAM e 1GB de swap. Definir os dois valores com o mesmo valor deixa o contentor sem swap. Definir mem_limit e deixar memswap_limit por definir permite novamente que o contentor use swap até ao tamanho do seu limite de memória.
Ubuntu 24.04 e Debian 13 usam cgroup v2 por predefinição, onde a swap é um contador separado (memory.swap.max), e isto funciona sem configuração adicional. A mensagem antiga Your kernel does not support swap limit capabilities surge em hosts com cgroup v1 arrancados sem swapaccount=1. Nesses hosts, o limite de memória continua a aplicar-se, mas a parte relativa à swap é ignorada.
Seja claro sobre o que a swap proporciona. Torna uma eliminação por OOM mais lenta, mas não menos provável, porque um processo com fuga de memória preenche a swap tão facilmente como preenche a RAM. Entretanto, um contentor que esteja a trocar dados continuamente na swap, usando o armazenamento partilhado da VPS, abranda todos os outros serviços no servidor. Para qualquer aplicação sensível à latência, um limite correto sem swap falha mais depressa e de forma mais previsível.
Por que o uso de memória parece pior do que é
No Linux, o valor MEM USAGE em docker stats subtrai apenas o cache de páginas inativo (inactive_file no cgroup v2). Por isso, o cache usado recentemente continua a ser contabilizado. Um contentor que lê ficheiros grandes aproxima-se do limite e permanece nesse nível. Isto é normal e não é uma fuga de memória, porque o cache limpo é recuperado antes de o OOM killer ser chamado. Um serviço como um servidor multimédia Jellyfin autoalojado parecerá estar permanentemente perto do limite exatamente por este motivo.
Separe o valor do cache e do conjunto de trabalho real a partir do contentor:
docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.eventsanon é memória anónima, o conjunto de trabalho que não pode ser libertado. file é cache de páginas, que pode ser libertado. Dimensione o limite com base em anon mais uma margem, e não no total. O ficheiro memory.events resolve a questão de forma conclusiva: um contador oom_kill acima de zero significa que o kernel terminou algo neste contentor desde que ele foi iniciado, e um contador max a aumentar significa que o contentor está a ser mantido no limite neste momento. Ambos os comandos precisam de uma shell e do coreutils dentro da imagem, por isso falham numa imagem distroless ou scratch.
Limites de dimensionamento num VPS de 8GB
Comece pelo host, não pelas aplicações. Num VPS de 8GB, reserve cerca de 1GB para o kernel, o daemon do Docker, o sshd, o journald e a sua própria shell de login. Isso deixa aproximadamente 7GB disponíveis, e a soma dos limites de todos os contentores deve permanecer abaixo desse valor. A sobrealocação funciona até ao dia em que dois serviços atingem o pico ao mesmo tempo.
Uma divisão funcional num servidor de 8GB:
- Reverse proxy: limite de 128m. É um processo pequeno, e um limite tão restrito deteta imediatamente uma recarga descontrolada da configuração.
- PostgreSQL: limite de 2g, com
shared_buffersdefinido para cerca de 512MB na configuração da base de dados. - Contentor da aplicação: limite de 1g.
- Worker em segundo plano: limite de 512m.
- Serviço de multimédia ou ficheiros: limite de 2g, sendo a maior parte utilizada pela cache de páginas.
Não copie estes valores para a sua própria stack. Execute os serviços sob carga real durante um dia, monitorize docker stats, registe o valor máximo de anon por contentor e acrescente aproximadamente mais metade como margem. Um limite demasiado apertado é pior do que não ter limite, porque termina um serviço saudável durante um pico normal de tráfego.
Há uma armadilha que merece uma nota própria. Alguns runtimes ignoram o limite, a menos que lhes indique que o devem utilizar, e os que o leem podem não dimensionar os próprios recursos da forma esperada. O PostgreSQL pode dimensionar shared_buffers e work_mem acima do limite do contentor e ser terminado. Uma JVM moderna (Java virtual machine) deteta o limite do cgroup por predefinição, mas builds HotSpot como o OpenJDK atribuem ao heap apenas 25 por cento desse valor. Por isso, -XX:MaxRAMPercentage=75 é a forma habitual de permitir que o heap utilize uma parte maior do limite, mantendo espaço para memória fora do heap. O Node.js dimensiona o heap predefinido a partir do limite do cgroup desde a versão 12, usando cerca de metade do limite do contentor e estabilizando perto de 2GB. Defina --max-old-space-size em megabytes apenas quando quiser um limite diferente e mantenha-o abaixo do limite do contentor, para que o garbage collector atue antes de o kernel intervir. No Ollama, a situação é semelhante, mas com outra opção, porque aumentar num_ctx faz crescer a cache KV em centenas de megabytes e o contentor termina a meio de um prompt longo. O cgroup não negoceia. Termina o processo.
Os limites de CPU funcionam de forma completamente diferente
cpus: "1.5" significa 150% de um core, aplicado como uma quota do CFS (completely fair scheduler). O contentor recebe 150ms de tempo de CPU em cada período de 100ms, partilhado por todas as suas threads. Quando esgota esse tempo, o kernel faz com que aguarde pelo período seguinte.
Esta é a diferença importante. Um contentor que ultrapassa o limite de memória é terminado. Um contentor que ultrapassa o limite de CPU sofre throttling e continua a funcionar, mas mais lentamente. Por isso, é seguro definir um limite de CPU agressivo, enquanto um limite de memória precisa de margem.
cpu_shares é uma ferramenta diferente: um peso relativo que só tem efeito quando as CPUs estão efetivamente saturadas. Dois contentores com shares de 1024 e 512 dividem um core ocupado aproximadamente na proporção de dois para um; num servidor sem carga, nenhum dos dois fica limitado. Use shares para definir a prioridade dos serviços e use cpus quando precisar de um limite efetivo, por exemplo para impedir que um trabalho noturno de transcodificação prive o servidor Web de recursos.
FAQ
O docker compose deploy.resources.limits funciona sem Docker Swarm?
Sim. O Docker Compose, o plugin docker compose (agora na linha v5), aplica deploy.resources.limits e deploy.resources.reservations quando executa docker compose up num único host. Confirme com docker inspect --format '{{.HostConfig.Memory}}' <container>, que apresenta o limite em bytes e apresenta 0 quando não foi aplicado nenhum limite. As chaves dentro de deploy concebidas para o Swarm são as chaves de cluster, como placement, update_config e endpoint_mode.
O que significa o código de saída 137 no Docker Compose?
Quase sempre significa que o processo principal recebeu SIGKILL, porque 137 é 128 mais o sinal 9 (um programa também pode terminar deliberadamente com 137, mas isso é raro). O kernel OOM killer é a causa habitual, mas um tempo limite de encerramento produz o mesmo código quando uma aplicação ignora SIGTERM. Execute docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> para distinguir os casos. true 137 indica uma eliminação por falta de memória, enquanto false 137 não indica.
Devo definir mem_limit ou deploy.resources.limits.memory?
Ambos funcionam com docker compose. deploy.resources.limits.memory é a forma atual da Compose Specification e é a melhor opção predefinida para um ficheiro novo. Mantenha mem_limit se o resto do ficheiro já utilizar as chaves de nível superior mais antigas. A Compose Specification exige que os dois valores sejam consistentes quando ambos estão definidos. Escolha um e confirme o resultado com docker inspect.
Porque é que o meu contentor está a utilizar todo o limite de memória sem ser terminado?
O valor de utilização em docker stats ainda inclui a page cache utilizada recentemente. O kernel liberta essa cache sob pressão em vez de provocar uma eliminação por OOM. Execute docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat e leia o valor anon, que corresponde ao working set que não pode ser recuperado. Um valor file elevado junto de um valor anon baixo indica um contentor a realizar entrada e saída de disco, não um contentor prestes a terminar.
Quanta RAM devo deixar não alocada numa VPS de 8GB?
Deixe cerca de 1GB para o kernel, o daemon do Docker, o sshd, o journald e a sua própria shell. Depois, mantenha a soma dos limites de todos os contentores abaixo dos 7GB restantes. Monitorize o valor máximo de anon de cada contentor sob carga real durante um dia antes de fixar os valores. Considere o total um orçamento, não um objetivo a preencher.