MÓDULO 01 · CONCEITO 06 DE 8

Containers em profundidade

Container não é "VM leve". É um processo Linux com isolamento controlado — e essa diferença muda tudo.

Tempo de leitura ~22 min Pré-requisito Linux para devs Próximo Git como ferramenta de pensamento

Existe uma maneira ingênua de pensar containers: "é tipo uma máquina virtual, só que mais leve". A frase não está errada — é só superficial. Containers e VMs resolvem o mesmo problema (isolamento de aplicações) por mecanismos radicalmente diferentes, e essa diferença mecânica tem consequências operacionais enormes — em segurança, performance, debugging e arquitetura. Quem trata containers como "VMs leves" eventualmente esbarra em situações onde a abstração vaza, e nessa hora não tem para onde fugir: precisa entender o que tem embaixo.

A boa notícia é que o que tem embaixo é compreensível. Containers em Linux são, basicamente, processos comuns com isolamento adicional aplicado via duas tecnologias do kernel: namespaces (que isolam o que o processo vê) e cgroups (que limitam o que ele consome). É isso. Docker, Podman, containerd, runc, Kubernetes — tudo o que parece magia é orquestração ao redor desses dois mecanismos. Quando você entende namespaces e cgroups, todo o resto faz sentido.

O modelo: processo + isolamento

Quando você roda docker run nginx, o que acontece no host Linux é, em essência, simples:

  1. Um processo é criado via fork() + exec() — é o nginx.
  2. Antes de exec, esse processo recebe um conjunto de namespaces novos: vê seu próprio sistema de arquivos, sua própria lista de processos, sua própria rede, etc.
  3. O processo é colocado em cgroups que limitam quanto pode consumir de CPU, memória e I/O.
  4. O sistema de arquivos é montado a partir de uma imagem (camadas de read-only mais uma camada writable em cima).

Pronto. É um processo nginx normal — você consegue ver no ps do host (com PID alto), pode kill dele, ele aparece no top. A única diferença para um nginx "normal" é que, do ponto de vista dele próprio, ele acha que é PID 1 num mundo onde só existe ele e seus filhos.

Compare com VMs: uma VM tem kernel próprio, um sistema operacional completo, hardware emulado. Boot leva segundos a dezenas de segundos. Memória mínima na casa de centenas de MB. Sandbox via hypervisor (KVM, Hyper-V) — isolamento muito mais forte, custo proporcionalmente maior. Container starta em milissegundos, consome dezenas de MB, mas compartilha o kernel do host — se há um exploit no kernel, ele atinge todos os containers ao mesmo tempo.

Namespaces — isolamento de visão

Linux suporta sete tipos de namespaces. Cada um isola um aspecto diferente do que o processo enxerga:

Cada container típico criado por Docker tem um namespace de cada tipo. O processo principal e seus filhos compartilham esses namespaces; processos fora deles não os veem. Você pode "entrar" num namespace existente via nsenter — é o que docker exec faz por baixo.

Experimento revelador: nsenter -t <pid_do_processo_no_container> -m -p -n -- /bin/bash te coloca dentro dos namespaces do container, sem usar Docker. Você vai ver o filesystem dele, os processos dele, a rede dele. É a prova de que Docker não é magia — é uma ferramenta sobre primitivas do kernel.

Cgroups — limitação de consumo

Namespaces controlam o que o processo ; cgroups controlam o que ele pode consumir. Cada cgroup pode ter limites em:

Em Kubernetes, resources.requests e resources.limits no manifesto do pod viram cgroups via runc. requests dão prioridade no scheduling; limits são os limites hard. App que tenta passar do limite de memória é OOMKilled imediatamente; app que tenta passar do limite de CPU é simplesmente desacelerado (throttled).

armadilha em produção

CPU throttling é silencioso. Seu app em Kubernetes com limits.cpu: 500m não recebe erro quando atinge o limite — ele simplesmente espera a próxima janela. O sintoma é latência alta com load aparentemente baixo. Métrica container_cpu_throttled_periods_total é o que mostra. Muitos times só descobrem isso quando aprendem a olhá-la.

Imagens OCI — o formato

Imagens de container seguem o padrão OCI (Open Container Initiative), uma especificação aberta mantida pela Linux Foundation. Uma imagem é essencialmente:

  1. Um manifest: metadados (qual arquitetura, qual SO).
  2. Um config: variáveis de ambiente, comando default, exposed ports, layers.
  3. Um conjunto de layers: arquivos tar.gz que, sobrepostos via union filesystem (overlay2), formam o rootfs.

O detalhe interessante é o sistema de camadas. Cada instrução RUN, COPY, ADD num Dockerfile cria uma nova camada. Camadas são imutáveis e identificadas por hash de conteúdo — duas imagens que compartilham camadas iniciais (mesma base, mesmas dependências) literalmente compartilham os bytes em disco e em transferência. Isso é o que torna pulls de imagens novas frequentemente rápidas: só camadas novas vêm.

Boas práticas de Dockerfile

Não é foco do conceito, mas vale o catálogo mínimo:

O problema do PID 1

Quando o processo principal de um container é PID 1, ele assume responsabilidades especiais que normalmente são do init do sistema:

Aplicações típicas (Node.js, Python, Java) não foram desenhadas para serem PID 1. Resultado: zumbis acumulam quando a aplicação cria sub-processos que terminam, e SIGTERM pode ser ignorado se a app não capturar explicitamente. A solução canônica é usar um init mínimo: tini, dumb-init, ou o --init do Docker. Em Kubernetes, o runtime moderno frequentemente já injeta um init.

Networking de containers

Cada container Docker num host típico tem sua própria network namespace, conectada ao host via veth pair (cabo virtual com duas pontas) e uma bridge (docker0 default). DNS interno resolve nomes de containers; NAT na bridge permite acessar internet.

Em Kubernetes, o modelo é diferente: cada pod tem network namespace, e todos os containers do mesmo pod compartilham essa namespace (podem conversar via localhost). Pods em hosts diferentes se conectam via overlay networks providas por CNI plugins (Calico, Cilium, Flannel) — geralmente VXLAN ou roteamento direto.

O que isso significa operacionalmente: containers podem fazer tudo que fazem com rede normal — abrir portas, conectar, escutar — mas dentro de uma rede virtual que é mapeada para a real via vários níveis de indireção. Quando algo falha em rede de container, debug envolve entender em qual camada (host, bridge, overlay, ingress) o pacote se perdeu.

Containers nas três linguagens da formação

Containerização é mais sobre o ecossistema do que sobre a linguagem, mas cada uma tem peculiaridades que valem nota:

C# / .NET
# Multi-stage build com SDK + runtime mínimo
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["app/app.csproj", "app/"]
RUN dotnet restore "app/app.csproj"
COPY . .
WORKDIR /src/app
RUN dotnet publish -c Release -o /publish

# Imagem final: chiseled (Microsoft) — minimalista, sem shell
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /publish .
USER app
ENTRYPOINT ["./app"]

Microsoft mantém imagens "chiseled": só o necessário para o runtime, sem shell, sem package manager. Reduz superfície de ataque dramaticamente. Para AOT (.NET 8+ Native AOT), pode usar distroless ou scratch.

Python
# Multi-stage com venv para isolar dependências
FROM python:3.13-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /venv && /venv/bin/pip install -r requirements.txt

FROM python:3.13-slim
WORKDIR /app
COPY --from=build /venv /venv
COPY . .
ENV PATH="/venv/bin:$PATH" PYTHONUNBUFFERED=1
USER 1000
ENTRYPOINT ["python", "-m", "app"]

PYTHONUNBUFFERED=1 é praticamente obrigatório em containers — sem isso, logs ficam em buffer e somem em crashes. python:3.13-slim é um bom default; distroless serve para apps maduros.

Go
# Build estático em multi-stage
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app -ldflags="-s -w" ./cmd/server

# Imagem final: scratch — vazia
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app /app
USER 1000
ENTRYPOINT ["/app"]

Go gera binário estático, então a imagem final pode ser literalmente vazia (scratch). Resultado: imagens de 10-20MB, superfície de ataque mínima. Lembre de copiar ca-certificates se o app fizer HTTPS.

Quando container não é a resposta

Containers viraram default da indústria, mas há cenários onde não cabem ou cabem mal:

Como praticar

  1. Crie um container do zero. Usando só unshare e chroot, crie um "container manual": um processo bash com namespace de PID, mount, network próprios. Você vai entender concretamente o que Docker faz.
  2. Inspecione a anatomia de uma imagem. docker save nginx:alpine -o nginx.tar, descomprima, navegue. Veja manifest.json, config.json, e cada layer. Cada camada é um tar.gz com diff de filesystem.
  3. Provoque OOMKill. Rode um container com --memory 100m e dentro dele um processo que aloca 200MB. Veja o kernel matar via dmesg | grep -i kill. Veja como o container reaparece (se gerenciado por orquestrador) ou morre permanentemente.

Referências para aprofundar

  1. livro Container Security — Liz Rice (2020). Liz Rice é referência. Capítulos 2-4 explicam namespaces, cgroups e como containers funcionam realmente. Curto, direto.
  2. livro Kubernetes in Action (2nd ed.) — Marko Lukša (2024). Para o passo seguinte. Capítulos sobre arquitetura interna do Kubernetes mostram como containers são orquestrados.
  3. livro The Linux Programming Interface — Michael Kerrisk. Capítulos 28 (clone, namespaces), 41 (cgroups). Para entender o que está embaixo da abstração.
  4. artigo Containers from Scratch — Liz Rice (palestra). YouTube. Liz Rice constrói "Docker" do zero usando syscalls em Go, ao vivo. Imperdível.
  5. artigo Linux Containers in 500 Lines of Code — Lizzie Dixon. lizzie.io/linux-containers-in-500-loc.html — implementa um container em C usando clone/setns/pivot_root. Leitura técnica recompensadora.
  6. artigo What Is a Container? — Jérôme Petazzoni. jpetazzo.github.io — Petazzoni foi um dos primeiros engineers de Docker. Suas explicações sobre internals são canônicas.
  7. artigo What every SRE should know about GNU/Linux memory management, OOMKiller and cgroups — Tanel Põder. Para entender por que apps morrem misteriosamente em pods. Memória + cgroups + OOM com profundidade.
  8. docs OCI Image Specification. github.com/opencontainers/image-spec — referência oficial. Curta. Lê em uma sessão.
  9. docs Linux man pages — namespaces(7), cgroups(7). man7.org — referência oficial. Para quem quer ir fundo no que cada namespace controla.
  10. docs Docker Engine Documentation. docs.docker.com — fonte primária. Best practices em Dockerfile estão aqui, atualizadas.
  11. vídeo Cgroups, namespaces and beyond — Jérôme Petazzoni. YouTube. 40 minutos densos sobre internals. Datado em pequenos detalhes mas conceitualmente atemporal.
  12. paper Borg, Omega, and Kubernetes — Burns et al. (Google, 2016). ACM Queue. História de como o Google chegou em Kubernetes via dez anos de Borg. Contexto fundacional.