Tutorial: configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian
Linux & Sistemas · Por Kevin Marcondes · Publicado em · 9 min de leitura
Neste passo a passo você vai configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian. Vamos criar slices dedicadas, apontar containers para essas slices e validar com arquivos do cgroup v2. O resultado: limites previsíveis e fáceis de manter, sem gambiarras específicas por container.
Pré-requisitos e o que vamos configurar
A meta é aplicar limites de CPU, memória e I/O em containers Docker usando cgroup v2 controlado pelo systemd. Vamos padronizar limites por slice (grupo de serviços) e, quando necessário, ajustar um container específico.
O que você precisa antes de começar:
- Debian recente (11 ou 12) com systemd e acesso sudo.
- Docker instalado e funcional. Se ainda não tiver, veja como instalar Docker e Docker Compose no Linux.
- Disponibilidade para um reboot caso precise habilitar cgroup v2.
- Conhecimento do dispositivo de disco usado pelo Docker (para limites de I/O).
Avisos importantes:
- Modificar parâmetros do bootloader e reiniciar pode afetar serviços em produção. Faça janela de manutenção.
- Limites de memória muito agressivos podem matar processos (OOMKill). Aplique gradualmente e monitore.
Confirmar (e habilitar) cgroup v2 no Debian
Precisamos do cgroup v2 em modo unificado. Valide a presença do arquivo cgroup.controllers e os controladores disponíveis. Se não estiver ativo, habilite via parâmetro do kernel e reinicie.
No Debian 12 geralmente já vem em v2. No Debian 11 pode ser necessário ajustar o GRUB. Reiniciar é obrigatório após alterar o GRUB.
# Verificar se cgroup v2 está ativo (deve listar controladores, ex.: cpu io memory)
cat /sys/fs/cgroup/cgroup.controllers
# Se o arquivo não existir ou estiver vazio, habilite cgroup v2 e reinicie
# (cuidado: ajuste de GRUB exige reboot)
sudo sed -i 's/^GRUB_CMDLINE_LINUX="\([^"]*\)"/GRUB_CMDLINE_LINUX="\1 systemd.unified_cgroup_hierarchy=1"/' /etc/default/grub
sudo update-grub
sudo reboot
Configurar Docker para usar driver de cgroup systemd
Para o Docker delegar corretamente os cgroups ao systemd, use o driver de cgroup "systemd". Também garantimos a diretiva Delegate do serviço docker. Após a alteração, recarregue o systemd e reinicie o Docker.
# Definir driver de cgroup = systemd
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}
}
JSON
# (Opcional, a maioria das instalações já possui Delegate=yes)
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/override.conf >/dev/null <<'INI'
[Service]
Delegate=yes
INI
sudo systemctl daemon-reload
sudo systemctl restart docker
# Conferir se o Docker está usando systemd como cgroup driver
docker info | grep -i 'Cgroup Driver'
Criar slices do systemd com ResourceControl (limites padrão)
Vamos criar slices para agrupar containers por função (ex.: produção e analytics). As propriedades ResourceControl do systemd traduzem para arquivos do cgroup v2.
Dica rápida para o disco do Docker: o caminho padrão é /var/lib/docker. O dispositivo de bloco associado (para IO*Max) pode ser descoberto com ferramentas como findmnt/lsblk.
Aplique limites gradualmente e monitore. Considere começar com MemoryHigh e só depois MemoryMax.
| Propriedade systemd | Efeito | Arquivo cgroup v2 | Observações |
|---|---|---|---|
| CPUQuota=50% | Quota de CPU (metade de 1 CPU) | cpu.max | Converte em quota/period (ex.: 50000 100000) |
| CPUWeight=200 | Peso relativo de CPU | cpu.weight | 1–10000; 100 = padrão; maior = mais prioridade |
| AllowedCPUs=0-1 | Pin de CPUs | cpuset.cpus.effective | Restringe a CPUs específicas |
| MemoryHigh=1G | Limite “soft” de memória | memory.high | Pressiona reclaim antes do hard limit |
| MemoryMax=2G | Limite “hard” de memória | memory.max | Ultrapassar pode matar processos (OOM) |
| MemorySwapMax=0 | Swap permitido | memory.swap.max | 0 desabilita swap; "max" sem limite |
| IOReadBandwidthMax=/dev/sda 10M | Limite de leitura (bytes/s) | io.max | Use o device correto (ex.: /dev/nvme0n1) |
| IOWriteBandwidthMax=/dev/sda 5M | Limite de escrita (bytes/s) | io.max | Pode usar IOPS com IO*IOPSMax |
# Descobrir device do /var/lib/docker (ajuste se necessário)
# DEV=$(lsblk -no PKNAME $(findmnt -n -o SOURCE /var/lib/docker) | head -n1); echo "/dev/${DEV}"
# Criar duas slices com limites distintos
sudo tee /etc/systemd/system/containers-prod.slice >/dev/null <<'UNIT'
[Unit]
Description=Slice para containers de producao
[Slice]
CPUQuota=200%
CPUWeight=500
MemoryHigh=4G
MemoryMax=6G
MemorySwapMax=1G
# Ajuste o device conforme seu disco do Docker
IOReadBandwidthMax=/dev/sda 30M
IOWriteBandwidthMax=/dev/sda 15M
UNIT
sudo tee /etc/systemd/system/containers-analytics.slice >/dev/null <<'UNIT'
[Unit]
Description=Slice para containers de analytics
[Slice]
CPUQuota=100%
CPUWeight=200
MemoryHigh=2G
MemoryMax=3G
# I/O mais restritivo
IOReadBandwidthMax=/dev/sda 10M
IOWriteBandwidthMax=/dev/sda 5M
UNIT
sudo systemctl daemon-reload
Executar containers dentro da slice (docker run e Compose)
Coloque o container dentro da slice com --cgroup-parent. Em Compose, use cgroup_parent no serviço. Assim, todos os processos do container herdam os limites da slice.
Se você mantém imagens privadas, pode interessar configurar um registry Docker privado com mirror. Para gestão visual, veja também como usar o Portainer.
# docker run com slice de producao
docker run -d --name web \
--cgroup-parent=containers-prod.slice \
-p 8080:80 nginx:alpine
# docker run com slice de analytics
docker run -d --name etl \
--cgroup-parent=containers-analytics.slice \
alpine:3 sh -c 'while true; do dd if=/dev/zero of=/tmp/file bs=1M count=50; sleep 5; done'
# docker compose (compose.yaml) — campo cgroup_parent por serviço
cat > compose.yaml <<'YML'
services:
api:
image: nginx:alpine
ports: ["8080:80"]
cgroup_parent: containers-prod.slice
worker:
image: alpine:3
command: ["sh","-c","while true; do sha256sum /dev/zero | head -c 100000 > /dev/null; done"]
cgroup_parent: containers-analytics.slice
YML
docker compose up -d
Ajustar limites de um container em execução (set-property)
Para um ajuste pontual sem recriar container, altere o scope do systemd gerado pelo Docker. O nome do scope segue o padrão docker-<CONTAINERID>.scope. Mudanças com --runtime são temporárias (até reboot); para persistir, prefira slices.
# ID curto do container
CID=$(docker ps -qf name=web)
SCOPE="docker-${CID}.scope"
# Ver o scope
systemctl status "$SCOPE" --no-pager
# Aplicar limites em runtime (exemplos)
sudo systemctl set-property --runtime "$SCOPE" CPUQuota=50%
sudo systemctl set-property --runtime "$SCOPE" MemoryMax=512M
# Ajuste o device conforme seu disco
sudo systemctl set-property --runtime "$SCOPE" IOReadBandwidthMax=/dev/sda 5M
Como confirmar que funcionou (verificação prática)
Valide com ferramentas do systemd e lendo arquivos do cgroup v2. Você deve ver o container dentro da slice e os arquivos cpu.max, memory.max e io.max refletindo os valores configurados.
Monitore continuamente. Um stack com métricas ajuda — veja um caminho em configurar Prometheus e Grafana com Docker Compose.
# Ver a hierarquia de cgroups/slices
systemd-cgls | sed -n '1,120p'
# Conferir uso ao vivo (cgroups)
systemd-cgtop | head -n 20
# Ler arquivos do cgroup v2 do container no slice
CID=$(docker ps -qf name=web)
CG="/sys/fs/cgroup/containers-prod.slice/docker-${CID}.scope"
printf "cpu.max: "; cat "$CG/cpu.max" || true
printf "memory.max: "; cat "$CG/memory.max" || true
printf "io.max: "; cat "$CG/io.max" || true
Solução de problemas comuns
- cgroup v2 ausente: confira /sys/fs/cgroup/cgroup.controllers. Se vazio, habilite cgroup v2 no GRUB e reinicie.
- "cgroup parent not found": a slice deve existir em /etc/systemd/system e o systemd precisa de daemon-reload.
- Sem efeito nos limites de I/O: provavelmente o device está errado. Descubra o device de /var/lib/docker e use-o nas diretivas IO*Max.
- Containers ignoram cgroup_parent: confirme que o driver de cgroup do Docker é systemd (docker info).
- OOM matando processos: comece com MemoryHigh e aumente MemoryMax gradualmente.
- Persistência: alterações via set-property em scopes são de runtime. Para manter após reinício do container/host, use slices + --cgroup-parent.
- Rootless Docker: o caminho e permissões de cgroups diferem; considere manter limites por slice no usuário do serviço. Para ambientes CI, veja dicas em BuildKit e cache remoto no CI Docker.
Perguntas frequentes
Preciso do Debian 12 para usar cgroup v2 com Docker?
Não necessariamente. O Debian 12 já vem com cgroup v2 por padrão. No Debian 11, você pode habilitar com systemd.unified_cgroup_hierarchy=1 no GRUB e reiniciar. Depois, configure o Docker para usar o driver de cgroup systemd.
Qual a diferença entre CPUQuota e CPUWeight?
CPUQuota define um teto absoluto de CPU (porcentagem). CPUWeight ajusta a prioridade relativa quando há contenção entre grupos. Use CPUWeight para equilíbrio entre serviços e CPUQuota para impor um limite rígido.
Como descobrir o device correto para IOReadBandwidthMax?
Descubra o dispositivo que sustenta /var/lib/docker com findmnt e lsblk. Exemplo: use findmnt -n -o SOURCE /var/lib/docker e então mapeie para o device de bloco principal com lsblk. Aplique o caminho, como /dev/sda ou /dev/nvme0n1.
Posso limitar um único container sem recriá-lo?
Sim. Use systemctl set-property no scope docker-<ID>.scope com --runtime para ajustes temporários. Para persistência entre reinícios, é melhor criar uma slice com os limites e iniciar o container com --cgroup-parent apontando para essa slice.
Compose suporta cgroup_parent?
Sim. Em Docker Compose v2 você pode usar a chave cgroup_parent no serviço. Todos os processos daquele serviço herdam a slice e os limites definidos. Garanta que o Docker esteja com driver de cgroup systemd.
Esses limites funcionam com drivers de armazenamento ou redes diferentes?
Limites de CPU e memória são independentes. Para I/O, os limites se aplicam ao device de bloco subjacente. Em storages com camadas (LVM, RAID, volumes remotos), avalie onde aplicar e confirme se o kernel expõe controles de io.* para o device escolhido.
Precisa de ajuda para aplicar limites sem interromper seu ambiente?
Se o seu cluster Docker precisa de limites previsíveis de CPU, memória e I/O, posso apoiar no diagnóstico e implementação gradual com systemd e cgroup v2.
Falar sobre seu casoVeja também
- tutorial: implementar Canary releases com Traefik em Docker Compose usando headers e weight routing para deploys graduais de aplicações PHP
- Tutorial: configurar HAProxy com Let's Encrypt e healthchecks para balanceamento de tráfego entre containers Docker em VPS (passo a passo)
- Tutorial prático: implementar mTLS entre Nginx e microserviços usando smallstep CA em Docker Compose
- Tutorial: automatizar snapshots LVM e uploads incrementais para S3 usando systemd timers e Restic no Debian