Docker é pesado em um VPS pequeno de 1 ou 2 GB?
Docker Engine no Linux não é uma máquina virtual. Veja como medir no seu próprio VPS de 1 ou 2 GB o que o daemon, os contêineres e as imagens realmente consomem.
Docker é pesado em um VPS pequeno?
O Docker Engine em um VPS pequeno não é pesado da forma que a fama sugere. Ele não cria um segundo sistema operacional: é um daemon somado a recursos que já existem no kernel, os namespaces, que isolam a visão que um processo tem de processos, rede, arquivos e usuários, e os cgroups (control groups), que contabilizam e limitam CPU, memória, I/O (entrada e saída de disco) e número de processos. Um contêiner rodando o nginx é o processo do nginx, visto por outra janela.
O peso que você vai sentir em uma máquina de 1 ou 2 GB vem de dois lugares. As imagens ocupam disco, e é isso que enche um disco de 20 ou 40 GB antes de qualquer outra coisa. Os processos dentro dos contêineres ocupam RAM, quase exatamente como ocupariam fora deles. O resto é pequeno, mas não é zero. Este guia mostra onde cada parte aparece e, principalmente, como medir tudo isso no seu próprio servidor.
O Docker que é pesado roda em Windows e macOS
Quase toda reclamação de que o Docker devora a máquina vem do Docker Desktop, e o Docker Desktop é outro produto. Namespaces e cgroups são recursos do kernel do Linux. O kernel do Windows e o do macOS não os têm. Para rodar um contêiner Linux nessas máquinas, o Docker Desktop precisa de um kernel Linux em algum lugar, então ele liga uma máquina virtual (VM) inteira, com memória reservada, e coloca o daemon lá dentro. Essa VM é o que aparece no gerenciador de tarefas consumindo gigabytes.
No seu VPS essa camada não existe. Você instala o pacote docker-ce, o daemon fala direto com o kernel que já está rodando, e não há interface gráfica, VM nem sincronização de arquivos entre dois sistemas. Se o servidor ainda está limpo, comece por instalar o Docker em um VPS pelo repositório oficial e volte para cá com o daemon no ar.
Meça o seu servidor em vez de acreditar em números de fórum
Qualquer número de consumo que você leia na internet está errado para o seu caso. O gasto do daemon muda com a versão do Docker, com a distribuição, com a versão de cgroup, com o driver de armazenamento, com quantos contêineres estão em execução e com quantas portas você publicou. Por isso este guia não traz uma tabela de megabytes. Ele traz os comandos que produzem os seus megabytes.
Quanto o daemon consome no seu VPS
systemctl status docker
systemctl show docker -p MemoryCurrent
systemctl show containerd -p MemoryCurrentMemoryCurrent vem do cgroup da própria unidade e é dado em bytes. Divida por 1048576 para ler em MiB. Se a unidade estiver parada, o valor volta vazio ou como um número absurdamente grande, que é o marcador de "não definido".
O dockerd não é o único processo em jogo, e essa é a parte que as pessoas esquecem. O containerd roda separado. Cada contêiner em execução tem um containerd-shim-runc-v2. Cada mapeamento de porta publicada pode ter um docker-proxy.
ps -o pid,rss,args -C dockerd -C containerd
ps -o pid,rss,comm -C containerd-shim-runc-v2
ps -o pid,rss,args -C docker-proxyA coluna RSS (resident set size, a memória residente do processo) vem em kilobytes. Some as linhas e você tem o custo fixo da sua instalação, medido na sua máquina. Para acompanhar isso ao vivo e agrupado por cgroup, rode systemd-cgtop -m --order=memory. Se ps -C docker-proxy não devolve nada, uma de duas coisas é verdade: você não publicou nenhuma porta, ou o proxy de userspace está desligado.
Memória por contêiner com docker stats
docker stats --no-stream
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.PIDs}}"A coluna MEM USAGE / LIMIT mostra o que o contêiner usa e o teto que ele tem. Se você nunca definiu um limite, o teto exibido é a RAM do host, porque, segundo a documentação de resource constraints do Docker, um contêiner não tem restrição de recursos por padrão e pode usar o que o kernel deixar. A coluna PIDS conta os processos vivos dentro do contêiner e costuma denunciar imagens que sobem mais coisa do que você imaginava. Para ver quais são: docker top nome-do-container e docker exec nome-do-container ps aux.
Por que docker stats não bate com free -h?
Os dois medem escopos diferentes, então somar um e comparar com o outro nunca fecha.
O docker stats lê os contadores do cgroup daquele contêiner e, no Linux, a CLI subtrai o cache antes de mostrar o valor: no cgroup v2 ela desconta o campo inactive_file do memory.stat, como está escrito na documentação do docker stats. Já o free -h mede a máquina inteira, incluindo o kernel, o dockerd, o containerd, o seu sshd e todo o cache de páginas contabilizado em buff/cache.
Disso saem duas consequências práticas. O daemon e os shims nunca aparecem no docker stats, porque não são contêineres, então o total dessa tela sempre subestima o que o Docker custa. E no free -h a coluna que interessa é available, não free, porque o Linux usa a RAM ociosa como cache de disco de propósito e devolve esse espaço no instante em que um processo pedir.
Para saber qual versão de cgroup o seu servidor usa, rode stat -fc %T /sys/fs/cgroup/. A resposta cgroup2fs significa v2, e tmpfs significa v1.
Quanto o Docker está ocupando de disco?
docker system df
docker system df -vO resumo traz as colunas TYPE, TOTAL, ACTIVE, SIZE e RECLAIMABLE. RECLAIMABLE é o que um prune conseguiria liberar agora. A versão -v abre imagem por imagem e separa SHARED SIZE, o espaço que uma imagem divide com outra, de UNIQUE SIZE, o espaço que só ela ocupa. Essa distinção é a razão de du -sh /var/lib/docker nunca bater com a soma das imagens listadas: as camadas são compartilhadas, e a soma conta a mesma camada várias vezes.
Repare na linha Build Cache. Ela é a surpresa de quem constrói imagens no próprio servidor, porque cresce a cada build e não desaparece quando você remove a imagem gerada.
Onde o custo aparece de verdade em um VPS pequeno
Disco: imagens, camadas e cache de build
Esse é o custo que realmente derruba um VPS de 20 ou 40 GB, e é o último para o qual as pessoas olham.
Imagens construídas sobre a mesma base compartilham camadas. Cinco serviços que partem do mesmo debian:bookworm-slim guardam essa base uma vez só. Já python:3.12 e node:22 não compartilham nada útil entre si, então cada um custa inteiro. Somado a isso, cada docker compose pull que traz uma tag nova deixa a imagem antiga no disco, agora sem tag, ocupando espaço até alguém limpar.
Em disco pequeno, a decisão que mais economiza é não construir no servidor. Faça o build em outra máquina, publique em um registry e use docker compose pull no VPS. Assim o cache de build simplesmente não existe lá.
Memória: o contêiner adiciona namespaces, não um segundo sistema
Um contêiner com Postgres consome quase o mesmo que o Postgres consumiria instalado pelo gerenciador de pacotes. Não há um segundo kernel, nem systemd, nem sshd, nem cron rodando dentro dele, a menos que a imagem coloque isso lá. O que o Docker acrescenta por contêiner é o containerd-shim-runc-v2, e você já mediu o tamanho dele com o ps da seção anterior.
Duas ressalvas honestas. Algumas imagens sobem mais processos do que o pacote da distribuição subiria, por exemplo um supervisord cuidando de dois serviços dentro do mesmo contêiner, e é por isso que a coluna PIDS do docker stats merece atenção. A segunda: rodar a mesma aplicação em dois contêineres não divide a memória anônima entre eles, porque são dois conjuntos de dados independentes. O código do binário, esse sim, é lido do mesmo arquivo na mesma camada, então fica no cache de páginas uma vez só.
Rede bridge e portas publicadas
A rede bridge padrão custa CPU, não memória. Todo pacote que chega em uma porta publicada passa por regras de NAT (network address translation, tradução de endereços de rede) no kernel. Veja as suas com sudo iptables -t nat -L DOCKER -n ou sudo nft list table ip nat, dependendo do que a sua distribuição usa.
Além do NAT, o Docker sobe por padrão um processo docker-proxy por mapeamento de porta. Em um VPS ocioso isso é barato. Em um servidor que atende muita conexão, aparece nos gráficos. Dá para desligar esse proxy em /etc/docker/daemon.json:
{
"userland-proxy": false
}Depois rode sudo systemctl restart docker. Reiniciar o daemon para e sobe os contêineres, então escolha um horário em que essa parada não incomode. A alternativa mais simples nem mexe no daemon: publique a porta apenas no loopback, com 127.0.0.1:8080:8080, e deixe um proxy reverso no host receber a internet. O NAT continua lá, mas a porta deixa de ficar exposta e o TLS (transport layer security) fica concentrado em um só lugar.
Escrita em disco: overlay2, volumes e bind mounts
O sistema de arquivos de um contêiner é uma pilha de camadas somente leitura com uma camada gravável em cima, montada pelo driver overlay2. Na primeira vez que um processo escreve em um arquivo que veio da imagem, o overlay copia o arquivo inteiro para a camada gravável. Isso se chama copy-up. Para um arquivo de configuração pequeno, ninguém percebe. Para um banco de dados de vários gigabytes deixado dentro da camada gravável, é uma tragédia de disco e de tempo.
A regra é direta: tudo que é escrito com frequência vai em um volume ou em um bind mount, que gravam direto no sistema de arquivos do host e não passam pelo overlay. Em Linux, bind mount não tem penalidade relevante de desempenho. A lentidão famosa de bind mounts é do Docker Desktop, onde os arquivos atravessam a fronteira entre o host e a VM.
O banco de dados é o caso que dói
Postgres e MySQL foram escritos para usar o cache de páginas do sistema operacional de forma agressiva. Dentro de um contêiner eles continuam usando o cache do mesmo kernel, então, sem limite configurado, nada muda. O problema nasce quando você coloca um limite de memória: no cgroup v2, o cache de páginas gerado pelo contêiner é cobrado no mesmo memory.max. Um limite apertado faz o kernel descartar o cache do banco o tempo todo, e o banco volta a ler do disco páginas que estavam na memória. A consulta fica lenta sem que nenhum erro apareça em lugar nenhum.
Há um segundo problema, e ele é silencioso. O Postgres dentro do contêiner enxerga a RAM total do host, não o limite do cgroup. Se você deixar shared_buffers em um valor calculado para a máquina inteira e der um limite pequeno ao contêiner, o processo tentará usar mais do que o cgroup permite e será morto. Configure shared_buffers e innodb_buffer_pool_size em função do limite do contêiner, não da RAM do plano. Se a escolha ainda está em aberto, comparar banco de dados em Docker contra banco no host cobre o resto do assunto.
O que fazer quando o VPS está apertado de verdade
Confirme que o problema é memória
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' nome-do-container
sudo dmesg -T | grep -iE 'out of memory|killed process'Um contêiner que sai com código 137 recebeu SIGKILL, porque 137 é 128 mais 9. Se OOMKilled vier true, quem matou foi o OOM killer (out of memory killer) do kernel por falta de memória, seja no limite do contêiner, seja na máquina inteira, e o dmesg mostra qual processo foi escolhido. Se vier false, o 137 provavelmente veio de um docker stop cujo tempo de espera acabou e escalou para SIGKILL, porque a aplicação ignorou o SIGTERM. São causas diferentes, com consertos diferentes.
Ponha limites antes que um serviço derrube o resto
Sem limite, um contêiner com vazamento de memória consome o VPS inteiro, e o OOM killer escolhe a vítima, que pode ser o seu banco de dados ou o sshd pelo qual você entra na máquina. Defina um teto por serviço e confirme na coluna LIMIT do docker stats que ele foi mesmo aplicado, porque a sintaxe do Compose tem armadilhas. Os detalhes estão em definir limites de memória no docker compose. Para o que roda fora do Docker no mesmo servidor, o equivalente é limitar memória e CPU de um processo com systemd.
Crie swap, nem que seja só uma rede de segurança
Um VPS de 1 GB sem swap não tem margem nenhuma: o primeiro pico mata um processo. Swap não deixa nada mais rápido. Ele troca uma morte instantânea por lentidão, e lentidão dá tempo de você perceber e agir.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -hO free -h deve passar a mostrar a linha Swap com o tamanho que você criou. Em virtualização que compartilha o kernel do host, como OpenVZ ou LXC, o swapon falha com swapon failed: Operation not permitted, e não existe contorno de dentro do servidor.
Limpe o disco com cuidado
docker system df
docker image prune
docker builder prunedocker image prune remove apenas as imagens sem tag. docker builder prune esvazia o cache de build, que é onde costuma estar o volume maior em quem constrói no servidor. docker system prune -a é bem mais agressivo: ele apaga toda imagem que não esteja em uso por um contêiner em execução, então o próximo up vai baixar tudo outra vez, o que pesa em link lento. Nunca acrescente --volumes sem saber exatamente o que há dentro deles, porque é ali que ficam os seus dados.
Junte serviços em vez de repetir pilhas inteiras
Três aplicações, cada uma com o seu próprio Postgres e o seu próprio Redis, é a forma mais comum de encher um VPS de 2 GB. Um Postgres servindo três bancos custa uma fração disso, e um proxy reverso só na frente de tudo custa outra fração. Enquanto você mede, subir um serviço de cada vez ajuda a enxergar o preço individual: subir apenas um serviço do docker compose mostra como fazer isso sem levantar a pilha toda.
Considere Podman rootless
O Podman não mantém um daemon de longa duração, então você não paga por um processo parado esperando comandos, e em modo rootless os contêineres rodam sem root. A troca é real: a rede rootless passa por pasta ou slirp4netns no espaço de usuário, o que custa CPU em cargas com muito tráfego. A comparação completa está em Podman contra Docker em um VPS.
Às vezes o Docker não é o problema
Se a sua aplicação precisa de mais RAM do que o plano tem, nenhum ajuste de contêiner resolve. Meça o processo sozinho, fora do Docker, e compare com a medida de dentro. Se a queixa for lentidão de CPU com carga baixa, o suspeito é outro: veja o que é steal time e como identificar um vizinho barulhento. E se o orçamento é o limite real, planos ARM costumam entregar mais memória pelo mesmo preço, desde que as suas imagens tenham build para arm64, assunto de a comparação entre VPS ARM e x86.
FAQ
O Docker consome muita RAM só por estar instalado?
Com nenhum contêiner rodando, o que fica de pé é o dockerd e o containerd. Meça no seu servidor com systemctl show docker -p MemoryCurrent e systemctl show containerd -p MemoryCurrent, que devolvem bytes, ou com ps -o pid,rss,args -C dockerd -C containerd, onde RSS vem em kilobytes. Esse é o piso da sua instalação, e é um número fixo que não cresce com o tempo. Se nem ele couber no plano, o caminho é o Podman, que não mantém daemon.
Por que o docker stats mostra um número e o free -h mostra outro?
Porque medem escopos diferentes. O docker stats lê os contadores do cgroup de cada contêiner e, no Linux, a CLI subtrai o cache inativo antes de exibir. O free -h mede a máquina inteira, incluindo o kernel, o daemon do Docker, os shims, o seu sshd e todo o cache de páginas em buff/cache. Some quanto quiser: os totais nunca vão fechar. No free -h, a coluna útil é available, que é a memória que um processo novo conseguiria pedir agora.
Meu contêiner morre com exit code 137. É culpa do Docker?
137 é 128 mais 9, ou seja, o processo recebeu SIGKILL. Rode docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' nome-do-container. Se OOMKilled for true, faltou memória, no limite do contêiner ou na máquina toda, e sudo dmesg -T | grep -iE 'out of memory' mostra qual processo o kernel escolheu matar. Se for false, a aplicação ignorou o SIGTERM enviado por um docker stop e foi morta quando o tempo de espera terminou.
Vale a pena usar Docker em um VPS de 1 GB?
Vale para um punhado de serviços pequenos, porque o custo do Engine é baixo perto do que as suas aplicações consomem e o ganho de organização é grande. Não vale se a ideia é subir uma pilha com banco de dados, fila, cache e proxy no mesmo giga de memória. Nesse caso o plano é que está pequeno, e o Docker não tem culpa. Antes de decidir, crie swap, defina limites por serviço e passe uma semana olhando docker stats e docker system df no seu próprio servidor.