Entendendo imagens base do Docker: o Ubuntu dentro do contêiner não é um Ubuntu de verdade
(oneuptime.com)- Mesmo ao executar
docker run ubuntu, ele compartilha o kernel Linux do host, e o Ubuntu fornece apenas as ferramentas de espaço de usuário - O resultado de
uname -rmostra a versão do kernel do host, e apenas/etc/os-releaseexibe informações do Ubuntu - VMs têm seu próprio kernel e levam minutos para inicializar, enquanto contêineres iniciam em milissegundos e, com isolamento em nível de SO sem virtualização de hardware, compartilham o kernel do host com baixo overhead
- Graças à estabilidade da ABI de system calls do Linux, contêineres de diferentes distribuições podem rodar sobre o mesmo kernel
- Em um ambiente com 16 GB de RAM, o limite prático costuma ser de 50-100 contêineres leves, 10-30 de porte médio e 5-10 contêineres grandes
- Entender essa arquitetura é importante porque vulnerabilidades no kernel afetam todos os contêineres, e a escolha da imagem base impacta diretamente compatibilidade e segurança
O que significa “executar Ubuntu”
- Ao executar
docker run ubuntu:22.04, você obtém um prompt bash que parece Ubuntu e pode usarapt updatee instalar pacotes - Porém, ao rodar
uname -rdentro do contêiner, aparece a versão do kernel do host (ex.: 6.5.0-44-generic) - O arquivo
/etc/os-releasemostra Ubuntu 22.04, mas o kernel é o da máquina host, e a parte “Ubuntu” não passa de um filesystem que compõe o espaço de usuário
Contêiner vs máquina virtual: comparação de arquitetura
- VMs virtualizam hardware; contêineres virtualizam o sistema operacional
- Principais diferenças:
- Kernel: cada VM tem seu próprio kernel; contêineres compartilham o kernel do host
- Tempo de boot: VMs levam minutos; contêineres levam milissegundos
- Overhead de memória: VMs usam 512 MB-4 GB; contêineres, 1-10 MB
- Uso de disco: VMs usam 10-100 GB; imagens de contêiner, 10-500 MB
- Nível de isolamento: VMs operam no nível de hardware; contêineres, no nível de processo
- Desempenho: VMs têm cerca de 5-10% de overhead; contêineres entregam desempenho próximo do nativo
Componentes reais de uma imagem base
- Conteúdo do tarball baixado ao fazer pull de
ubuntu:22.04: -
1. Binários essenciais (
/bin,/usr/bin)/bin/bash(shell),/bin/ls(lista arquivos),/bin/cat(exibe arquivos)/usr/bin/apt(gerenciador de pacotes),/usr/bin/dpkg(ferramenta de pacotes Debian)
-
2. Bibliotecas compartilhadas (
/lib,/usr/lib)- glibc e outras bibliotecas compartilhadas às quais os programas se vinculam
/lib/x86_64-linux-gnu/libc.so.6(biblioteca C - base de todos os programas em C)- Bibliotecas essenciais como
libpthread.so.0elibm.so.6
-
3. Arquivos de configuração (
/etc)/etc/apt/sources.list(repositórios de pacotes)/etc/passwd(banco de dados de usuários)/etc/resolv.conf(configuração de DNS, geralmente montada a partir do host)
-
4. Banco de dados de pacotes
/var/lib/dpkg/status(pacotes instalados)/var/lib/apt/lists/(cache de pacotes disponíveis)
- Kernel, bootloader e drivers não estão incluídos
O kernel permanece, todo o resto muda
- Funções fornecidas pelo kernel Linux: agendamento de processos, gerenciamento de memória, operações de filesystem, pilha de rede, drivers de dispositivo e system calls
- Quando um processo no contêiner chama
open(),read()oufork(), isso é encaminhado diretamente ao kernel do host - O kernel não sabe nem se importa se aquele processo está em um “contêiner Ubuntu” ou “contêiner Alpine”
-
Estabilidade da interface de system calls
- A ABI de syscalls do Linux é muito estável
- Motivos pelos quais um binário compilado com glibc 2.31 (Ubuntu 20.04) ainda funciona em um kernel do Ubuntu 24.04:
- o kernel mantém compatibilidade retroativa
- os números de system calls não mudam
- novos recursos são adicionados, mas recursos antigos quase nunca são removidos
- É por isso que é possível rodar um contêiner Ubuntu 18.04 em um host com kernel 6.5
Testando na prática: mesmo kernel, espaço de usuário diferente
- Ao consultar o mesmo kernel em várias imagens base, é possível ver que todas compartilham o kernel do host
ubuntu:22.04,debian:12,alpine:3.19,fedora:39earchlinux:latestmostram a mesma versão de kernel (6.5.0-44-generic)- O que muda entre os contêineres é a composição do userland, como o binário
unamee a libc
Por que contêineres são tão eficientes
-
1. Sem duplicação de kernel
- Cada VM carrega o kernel inteiro na memória (cerca de 100-500 MB)
- 10 VMs consomem memória com 10 kernels; 10 contêineres usam apenas um kernel
-
2. Inicialização instantânea
- Sequência de boot de uma VM: BIOS → bootloader → kernel → sistema init → serviços
- Um contêiner precisa apenas de chamadas
fork()eexec()para que o processo exista em milissegundos - Boot típico de VM: 30-60 segundos / início de contêiner: cerca de 0,347 s
-
3. Camadas de imagem compartilhadas
- Ao rodar 100 contêineres com
ubuntu:22.04, a camada base da imagem existe apenas uma vez no disco - Cada contêiner ganha apenas uma fina camada copy-on-write para suas alterações
- Ao rodar 100 contêineres com
-
4. Compartilhamento de memória via kernel
- O page cache do kernel é compartilhado
- Se 50 contêineres lerem o mesmo arquivo, o kernel o mantém em cache apenas uma vez
- Ao usar as mesmas bibliotecas compartilhadas, páginas de memória podem ser compartilhadas com copy-on-write
Calculando o limite de execução de contêineres
-
Análise de memória (com base em uma VM com 16 GB de RAM)
- RAM total: 16.384 MB
- Overhead do SO host: -1.024 MB
- Daemon do Docker: -256 MB
- Overhead do runtime de contêineres: -512 MB
- Memória disponível para contêineres: 14.592 MB
-
Uso de memória por tipo de contêiner
- Mínimo (
sleep): cerca de 1 MB - Alpine + app pequena: cerca de 25 MB
- Ubuntu + app Python: cerca de 120 MB
- Ubuntu + app Java: cerca de 500 MB
- Serviço Node.js: cerca de 200 MB
- Mínimo (
-
Máximo teórico
- Contêiner mínimo (1 MB): 14.592
- Alpine + app pequena (25 MB): 583
- Ubuntu + Python (120 MB): 121
- Microsserviço Java (500 MB): 29
-
Limites reais
- Além da memória, é preciso considerar:
- Escalonamento de CPU: concorrência excessiva entre contêineres causa picos de latência
- Descritores de arquivo:
ulimitpadrão de 1024 - Portas de rede: o mapeamento de portas só pode usar 65.535 portas
- PIDs: limite de
/proc/sys/kernel/pid_max(padrão: 32.768) - I/O de disco: overhead do OverlayFS e necessidade de percorrer muitas camadas
- Em uma VM de 16 GB rodando cargas reais, o limite prático costuma ser:
- Contêineres leves (API, workers): 50-100
- Contêineres médios (DB, cache): 10-30
- Contêineres grandes (modelos de ML, apps JVM): 5-10
- Além da memória, é preciso considerar:
Compatibilidade entre distribuições Linux
-
A promessa da ABI do kernel
- O Linux mantém uma interface de syscalls estável
- Binários compilados para kernels antigos funcionam em kernels novos
- Um binário do Ubuntu 18.04 roda normalmente em um kernel 6.5
-
Quando a compatibilidade quebra
- Requisitos de recursos do kernel: quando o contêiner precisa de um recurso ausente no kernel (ex.: io_uring requer kernel 5.1+)
- Dependência de módulos de kernel: Wireguard exige o módulo de kernel wireguard; contêineres NVIDIA exigem o driver de kernel nvidia
- Restrições de seccomp/capabilities: quando o host bloqueia syscalls necessárias ao contêiner (ex.: usar
ptracerequer--cap-add SYS_PTRACE)
Guia de escolha de imagem base
| Imagem base | Tamanho | Gerenciador de pacotes | Uso |
|---|---|---|---|
scratch |
0 MB | nenhum | binários Go/Rust compilados estaticamente |
alpine |
7 MB | apk | contêineres mínimos, musl libc |
distroless |
20 MB | nenhum | foco em segurança, sem shell nem gerenciador de pacotes |
debian-slim |
80 MB | apt | equilíbrio entre tamanho e compatibilidade |
ubuntu |
78 MB | apt | praticidade para desenvolvimento |
fedora |
180 MB | dnf | pacotes mais novos, SELinux |
-
Quando usar cada imagem
- scratch: para binários compilados estaticamente, contendo apenas o binário e nenhum SO
- alpine: imagem mínima quando é preciso acesso a shell; usa musl libc em vez de glibc, o que pode causar alguns problemas de compatibilidade
- distroless: imagem de produção focada em segurança; sem shell e sem gerenciador de pacotes, é mais difícil de depurar, mas mais segura
A fronteira entre espaço de usuário e kernel
-
O que vem da imagem base (espaço de usuário)
- shell (
/bin/bash,/bin/sh) - biblioteca C (glibc, musl)
- gerenciador de pacotes (apt, apk, yum)
- utilitários principais (ls, cat, grep)
- configuração do sistema init (geralmente não o próprio systemd)
- usuários e grupos padrão (
/etc/passwd) - caminhos e configurações de bibliotecas
- shell (
-
O que vem do host (kernel)
- escalonamento de processos e gerenciamento de memória
- pilha de rede (TCP/IP, roteamento)
- operações de filesystem (leitura, escrita, montagem)
- recursos de segurança (namespaces, cgroups, seccomp)
- drivers de dispositivo (GPU, rede, armazenamento)
- gerenciamento de tempo e clock
- criptografia e geração de números aleatórios
-
A ilusão criada pelos namespaces
- O kernel fornece namespaces para fazer o contêiner parecer isolado
- Um processo que aparece como PID 1 dentro do contêiner existe no host com um PID mais alto (ex.: 45678)
- O kernel mantém esse mapeamento: PID 1 do contêiner → PID 45678 no host
- É assim que o isolamento funciona sem virtualização
O que isso significa em produção
-
1. Vulnerabilidades de kernel afetam todos os contêineres
- Se houver uma vulnerabilidade no kernel do host, todos os contêineres ficam expostos
- Manter o host corrigido é essencial
-
2. O kernel do host limita os recursos do contêiner
- Para usar
io_uring, o host precisa de kernel 5.1+ - Recursos de eBPF exigem kernel 4.15+ com opções específicas ativadas
- Para usar
-
3. A importância de glibc vs musl
- Alpine usa musl libc
- Alguns binários compilados para glibc podem não funcionar
- Ex.: ao executar um binário glibc no Alpine, pode ocorrer erro de ausência do arquivo
/lib/x86_64-linux-gnu/libc.so.6
-
4. O “SO” do contêiner é um conceito puramente organizacional
- Do ponto de vista do kernel, não há diferença entre um “contêiner Ubuntu” e um “contêiner Debian”
- Ambos são apenas processos emitindo syscalls
Equívocos comuns
- ❌ “Contêineres são VMs leves”: contêineres são processos com isolamento avançado; VMs virtualizam hardware e executam um kernel separado
- ❌ “Cada contêiner tem seu próprio kernel”: todos os contêineres compartilham o kernel do host; o “SO” do contêiner é apenas o conjunto de arquivos de espaço de usuário
- ❌ “Executar um contêiner Ubuntu = executar Ubuntu”: você executa ferramentas Ubuntu sobre o kernel do host; se o host for Debian, na prática é um kernel Debian
- ❌ “A imagem base contém um sistema operacional completo”: a imagem base inclui apenas ferramentas mínimas de espaço de usuário; não inclui kernel, bootloader nem drivers
- ❌ “Mais contêineres = mais memória”: com camadas compartilhadas e page cache do kernel, os contêineres frequentemente compartilham memória com eficiência
Resumo principal
- Uma imagem base do Docker é um snapshot do filesystem com os componentes de espaço de usuário de uma distribuição Linux
- binários, bibliotecas e configurações que fazem o Ubuntu parecer Ubuntu
- O verdadeiro sistema operacional, o kernel, é compartilhado com o host
- Essa arquitetura permite:
- tempo de inicialização em milissegundos (sem boot de kernel)
- overhead mínimo de memória (um kernel, páginas compartilhadas)
- alta densidade (centenas de contêineres por host)
- desempenho próximo do nativo (syscalls diretas ao kernel)
- O trade-off é um isolamento mais fraco do que o de VMs: como os contêineres compartilham o kernel, um exploit no kernel afeta todos os contêineres
- Para a maioria das cargas de trabalho, esse trade-off vale a pena
9 comentários
Kernel + ferramentas = distribuição
Então, nesse caso, isso também não seria Ubuntu..
Então, parece que até existem tutoriais de como criar o Docker diretamente no Linux, isolando diretórios e mexendo com usuário, grupo e essas coisas.
Muito útil.
리눅스 네임스페이스, cgroups, 및 chroot를 사용하여 자체 Docker를 구축하세요.
Então, para exagerar um pouco, dá até para ver como
chroot + cgroup = docker.Na verdade, isso é um pouco mais próximo de
systemd-nspawn☝️🤓Na verdade
Sarcasmo / Autodepreciação
Realmente é muito interessante.
Parece que o texto explica com base no Linux,
mas, no caso de executar no Windows, então ele estaria compartilhando um kernel virtual criado pelo WSL2, como no artigo, certo?
Se surgir uma vulnerabilidade no Docker que permita mexer no kernel, também fico curioso se, em termos de segurança, o Windows, por ter uma camada extra de virtualização em relação ao Linux, poderia ser considerado mais robusto.
Fiquei um pouco surpreso com a reação do comentário de cima.
Eu achei que todo mundo usasse isso já sabendo disso.
O kernel Linux é o do host — o resto vem na forma de ferramentas usadas nas distribuições Linux.
Pelo que eu sei, o WSL2 roda virtualizado no Hyper-V.
Windows - Linux na máquina virtual - e dentro dele, de novo, Container...
Basicamente, o
rootdentro do Container não é o verdadeirorootdo sistema inteiro, então por padrão ele não consegue manipular o kernel de forma arbitrária.Mas, se surgir alguma vulnerabilidade, aí pode dar um problemão.
Do ponto de vista de desempenho, no Windows isso acaba sendo um pouco mais lento porque passa por uma camada extra de virtualização.