Arquitetura de sistemas Backend Docker Infraestrutura

Docker por baixo dos panos: o que realmente acontece quando você executa um container?

Compartilhar: X / Twitter LinkedIn

Docker costuma ser apresentado de uma forma bem simples:

docker run nginx

Pronto. Alguns segundos depois, temos um servidor web funcionando.

Mas o que realmente aconteceu nesse intervalo?

Docker não cria uma máquina virtual tradicional. Na verdade, ele combina vários recursos existentes no sistema operacional — principalmente no kernel Linux — para criar processos isolados que parecem estar executando em ambientes completamente independentes.

Neste artigo vamos entender como isso funciona por baixo dos panos e executar alguns comandos para enxergar esses mecanismos funcionando na prática.


Container não é uma máquina virtual

Antes de entrar nos detalhes, precisamos desfazer uma confusão bastante comum.

Uma máquina virtual executa um sistema operacional completo sobre um hypervisor.

De forma simplificada:

Servidor físico
    |
Hypervisor
    |
+-------------+
| VM          |
| Linux       |
| Aplicação   |
+-------------+

Containers funcionam de outra maneira.

Eles compartilham o kernel do sistema operacional hospedeiro:

Servidor
   |
Kernel Linux
   |
Docker
   |
+-----------+  +-----------+
| Container |  | Container |
| App A     |  | App B     |
+-----------+  +-----------+

Um container é, essencialmente, um processo do sistema operacional executando com várias formas de isolamento.

Isso explica por que containers normalmente inicializam muito mais rápido que máquinas virtuais.


O que acontece quando executamos docker run?

Vamos começar com:

docker run nginx

Esse comando aparentemente simples inicia uma cadeia de operações.

De maneira resumida:

docker CLI
   |
   v
Docker Engine
   |
   v
containerd
   |
   v
runc
   |
   v
Linux Kernel

Cada uma dessas peças possui uma responsabilidade.


Docker CLI

Quando digitamos:

docker run nginx

estamos utilizando o Docker CLI.

O CLI não cria o container diretamente.

Ele envia uma requisição para o Docker Engine, normalmente através de um socket Unix:

/var/run/docker.sock

Podemos verificar se esse socket existe:

ls -l /var/run/docker.sock

É através dele que vários comandos Docker se comunicam com o daemon.


Docker Engine

O Docker Engine, tradicionalmente representado pelo processo dockerd, recebe as solicitações feitas pelo CLI.

Podemos procurar pelo processo:

ps aux | grep dockerd

O daemon é responsável por tarefas como:

  • gerenciamento de imagens;
  • redes;
  • volumes;
  • configuração de containers;
  • comunicação com runtimes de container.

Mas ele também não executa diretamente o processo final do container.

Para isso entra outra peça importante.


containerd

O containerd é responsável pelo ciclo de vida dos containers.

Podemos verificar se ele está rodando:

ps aux | grep containerd

Entre suas responsabilidades estão:

  • criação de containers;
  • gerenciamento de imagens;
  • armazenamento;
  • execução;
  • supervisão dos processos.

O Docker utiliza o containerd como runtime de alto nível.

Mas ainda existe mais uma camada.


runc

O runc é um runtime de baixo nível responsável por efetivamente criar o processo isolado no Linux.

Ele segue a especificação OCI, a Open Container Initiative, que define padrões para execução de containers.

No final das contas, o runc conversa com o kernel Linux e utiliza mecanismos como:

  • namespaces;
  • cgroups;
  • capabilities;
  • filesystem em camadas.

E aqui começamos a chegar ao coração dos containers.


Namespaces: criando a ilusão de um sistema independente

Namespaces são funcionalidades do kernel Linux responsáveis por isolar recursos.

Um processo dentro de um container pode acreditar que possui:

  • seus próprios processos;
  • sua própria rede;
  • seu próprio hostname;
  • seus próprios mounts.

Mesmo estando no mesmo sistema operacional.

Entre os namespaces mais importantes estão:

PID     -> processos
NET     -> rede
MNT     -> filesystem e mounts
UTS     -> hostname
IPC     -> comunicação entre processos
USER    -> usuários

Vamos observar isso funcionando.


Testando isolamento de hostname

Execute:

docker run --rm alpine hostname

Você verá algo parecido com:

7b0b14a984e1

Agora execute:

hostname

O hostname da sua máquina será diferente.

O container possui seu próprio namespace UTS.

Também podemos alterar o hostname do container:

docker run --rm --hostname meu-container alpine hostname

Resultado:

meu-container

O hostname da máquina continua intacto.


PID Namespace: isolamento de processos

Agora execute:

docker run --rm alpine ps

Você verá poucos processos.

Algo semelhante a:

PID   USER     TIME  COMMAND
1     root     0:00  ps

Mas sua máquina possui dezenas ou centenas de processos.

Por quê?

Porque o container possui seu próprio PID namespace.

Dentro daquele namespace, o processo principal recebe o PID:

1

Porém, no host, esse mesmo processo possui outro PID.

Vamos testar.

Execute:

docker run -d --name teste alpine sleep 1000

Agora:

docker exec teste ps

Dentro do container podemos ver:

PID
1

Agora procure o processo no host:

ps aux | grep "sleep 1000"

Você encontrará um PID completamente diferente.

É o mesmo processo observado a partir de namespaces diferentes.

Depois do teste:

docker rm -f teste

Cgroups: controlando recursos

Namespaces isolam.

Mas isolamento sozinho não resolve outro problema.

Imagine que um container entre em loop e tente consumir toda a memória disponível.

É aí que entram os cgroups, ou Control Groups.

Eles permitem controlar recursos como:

  • CPU;
  • memória;
  • quantidade de processos;
  • I/O.

Podemos limitar um container, por exemplo, a 256 MB de memória:

docker run --rm -m 256m nginx

Também podemos limitar CPU:

docker run --rm --cpus="0.5" nginx

Nesse caso, o container poderá utilizar aproximadamente metade de um núcleo de CPU.

Podemos visualizar os limites configurados usando:

docker inspect <container>

Ou, com o container executando:

docker stats

O comando mostra informações como:

CPU %
Memory Usage
Memory Limit
Network I/O
Block I/O

Isso é possível graças aos cgroups do kernel Linux.


Imagens Docker são formadas por camadas

Outra parte interessante do Docker são as imagens.

Uma imagem não costuma ser um grande arquivo contendo todo o sistema.

Ela é formada por várias camadas.

Considere este Dockerfile:

FROM node:22-alpine

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

CMD ["node", "index.js"]

Cada instrução pode contribuir para a criação de uma camada.

Podemos visualizar as camadas de uma imagem:

docker history nginx

Você verá algo semelhante a:

IMAGE          CREATED        CREATED BY
abc123         ...            CMD [...]
def456         ...            COPY ...
ghi789         ...            RUN ...

Isso permite que várias imagens reutilizem camadas já existentes.

Por exemplo, duas aplicações baseadas em:

FROM node:22-alpine

podem compartilhar várias camadas da imagem base.

Isso economiza:

  • espaço em disco;
  • banda;
  • tempo durante builds.

OverlayFS e o filesystem do container

No Linux, Docker frequentemente utiliza um filesystem chamado OverlayFS.

A ideia é sobrepor diferentes camadas.

Podemos imaginar algo assim:

Container Layer
      |
Application Layer
      |
Dependencies Layer
      |
Base Image

As camadas da imagem são somente leitura.

Quando o container é criado, uma nova camada gravável é adicionada.

Por isso dois containers podem utilizar a mesma imagem sem modificar um ao outro.

Por exemplo:

docker run --name container1 -d nginx
docker run --name container2 -d nginx

Os dois utilizam a mesma imagem.

Agora altere algo dentro do primeiro:

docker exec container1 sh -c "echo teste > /tmp/arquivo.txt"

Verifique:

docker exec container1 cat /tmp/arquivo.txt

Resultado:

teste

Agora tente:

docker exec container2 cat /tmp/arquivo.txt

O arquivo não existirá.

Isso acontece porque cada container possui sua própria camada gravável.

Depois:

docker rm -f container1 container2

Containers são processos

Talvez essa seja a parte mais importante de entender.

Um container não é uma entidade mágica.

Ele continua sendo um processo do sistema operacional.

Vamos provar isso.

Execute:

docker run -d --name nginx-teste nginx

Agora:

docker top nginx-teste

O Docker mostrará os processos que estão executando dentro do container.

Também podemos obter o PID principal:

docker inspect nginx-teste

Procure pelo campo:

"Pid"

Em sistemas Linux, também podemos consultar diretamente:

docker inspect -f '{{.State.Pid}}' nginx-teste

Imagine que o resultado seja:

15432

Agora execute:

ps -p 15432

O processo aparecerá normalmente no sistema operacional.

Ou seja:

Container
=
Processo
+
Namespaces
+
Cgroups
+
Filesystem isolado
+
Configurações de segurança

Essa definição, embora simplificada, explica muito bem o conceito.


Como funciona a rede Docker?

Containers também possuem seu próprio namespace de rede.

Quando instalamos Docker, normalmente uma interface chamada:

docker0

é criada.

Podemos verificar:

ip addr show docker0

O Docker utiliza uma bridge virtual para conectar containers.

Uma visão simplificada seria:

Container
eth0
 |
veth
 |
docker0
 |
Host
 |
Internet

Cada container recebe uma interface virtual.

Execute:

docker run --rm alpine ip addr

Você verá uma interface semelhante a:

eth0

Ela existe apenas dentro do namespace de rede daquele container.


Publicando portas

Quando executamos:

docker run -p 8080:80 nginx

estamos dizendo:

Host:8080
    |
    v
Container:80

Agora podemos acessar:

http://localhost:8080

O nginx continua ouvindo na porta 80 dentro do container.

Docker cria as regras necessárias para que o tráfego destinado à porta 8080 do host chegue ao container.

Podemos verificar as portas:

docker port <container>

Volumes existem porque containers são descartáveis

A camada gravável do container existe enquanto o container existir.

Se removermos o container:

docker rm

essa camada também desaparece.

Por isso dados persistentes geralmente devem ficar fora dela.

É aí que entram os volumes.

Podemos criar um volume:

docker volume create meus-dados

Listar:

docker volume ls

Agora podemos utilizá-lo:

docker run --rm \
  -v meus-dados:/dados \
  alpine \
  sh -c "echo 'Olá Docker' > /dados/teste.txt"

Mesmo depois que esse container terminar, o volume continua existindo.

Podemos provar:

docker run --rm \
  -v meus-dados:/dados \
  alpine \
  cat /dados/teste.txt

Resultado:

Olá Docker

O primeiro container já não existe.

Mas os dados continuam lá.


Podemos enxergar os namespaces diretamente?

Em sistemas Linux, sim.

Execute um container:

docker run -d --name namespace-test alpine sleep 1000

Pegue seu PID:

PID=$(docker inspect -f '{{.State.Pid}}' namespace-test)

Agora:

ls -l /proc/$PID/ns

Você verá algo semelhante a:

ipc
mnt
net
pid
user
uts

Esses arquivos representam os namespaces associados ao processo.

Essa talvez seja uma das melhores demonstrações de que Docker utiliza mecanismos do próprio Linux para criar containers.

Depois:

docker rm -f namespace-test

E onde entra o Docker Desktop?

Em Linux, Docker consegue utilizar diretamente os recursos do kernel.

No Windows e no macOS existe uma diferença importante.

Como containers Linux dependem de funcionalidades específicas do kernel Linux, o Docker Desktop normalmente executa uma pequena máquina virtual Linux internamente.

A arquitetura fica aproximadamente assim:

Windows/macOS
     |
Docker Desktop
     |
Linux VM
     |
Docker Engine
     |
containerd
     |
runc
     |
Containers

No Windows, por exemplo, Docker Desktop pode utilizar WSL2 para fornecer esse ambiente Linux.

Isso significa que containers continuam funcionando com os mesmos conceitos de namespaces e cgroups, mas existe uma camada Linux intermediária.


Recapitulando

Quando executamos:

docker run nginx

muita coisa acontece.

De maneira bastante simplificada:

docker CLI
    |
    v
Docker Engine
    |
    v
containerd
    |
    v
runc
    |
    v
Linux Kernel
    |
    +-- namespaces
    +-- cgroups
    +-- filesystem
    +-- networking
    |
    v
Processo nginx

O kernel cria os mecanismos de isolamento.

Namespaces fazem o processo enxergar apenas determinados recursos.

Cgroups controlam quanto CPU e memória ele pode consumir.

O filesystem em camadas permite reutilizar imagens.

A rede virtual conecta o container ao host e a outros containers.

E o processo principal da aplicação continua sendo apenas isso:

um processo Linux.


Uma boa definição de container

Depois de conhecer o funcionamento interno, podemos abandonar aquela ideia de que container é uma espécie de “mini máquina virtual”.

Uma definição mais próxima da realidade seria:

Um container é um processo isolado utilizando mecanismos do kernel do sistema operacional.

Docker fornece toda a experiência em volta disso:

build
pull
push
run
network
volume
registry
logs
exec

Mas o isolamento fundamental vem do sistema operacional.

E quando entendemos isso, muitos comportamentos aparentemente misteriosos do Docker começam a fazer muito mais sentido.

Escrito por

Gabriel Stamato

Profissional de tecnologia com experiência em liderança técnica e gestão de produto, atuando na construção de estratégias com foco em qualidade, escalabilidade e visão de longo prazo. Como Tech Lead, fez a ponte entre negócio e tecnologia, ajudando a transformar necessidades do cliente em direcionamentos claros para o time de desenvolvimento, além de alinhar expectativas entre áreas e apoiar a tomada de decisão técnica. Também atuou na padronização de tecnologias, boas práticas de desenvolvimento e uso estratégico de serviços em cloud, sempre buscando eficiência e melhor custo-benefício para a empresa. Possui ainda experiência com DevOps, AWS e pipelines de entrega contínua, contribuindo para operações mais estáveis e escaláveis.

Deixe um comentário

Rolar para cima