Introdução
Os conceitos e a arquitetura antes dos comandos.
Introdução ao Docker
“Na minha máquina funciona.” Essa frase resume sozinha o problema que o Docker ataca. Uma aplicação nunca se resume ao seu código: ela depende de uma versão de interpretador, de uma biblioteca do sistema, de uma variável de ambiente, de um arquivo de configuração colocado no lugar certo, de um serviço de banco de dados acessível em uma porta específica. Esse halo de dependências quase nunca está escrito em lugar nenhum. Ele vive na máquina da pessoa que fez o projeto funcionar primeiro e é reconstituído manualmente, com mais ou menos sorte, em cada nova máquina.
A consequência não é apenas um momento desagradável instalando um projeto. É uma classe inteira de bugs que só existem em um determinado ambiente: um teste que passa localmente e falha na integração, uma correção validada em homologação que quebra em produção porque a versão do OpenSSL não é a mesma. Enquanto o ambiente de execução permanecer implícito, ele continua sendo uma variável não controlada do seu sistema, e você acaba depurando diferenças em vez de código.
O Docker propõe uma resposta simples de enunciar: tornar o ambiente explícito, versionado e transportável, da mesma forma que o código. Descreve-se em um arquivo de texto tudo o que a aplicação precisa para rodar, constrói-se a partir dele um artefato imutável e executa-se esse artefato exatamente como ele é em qualquer lugar — máquina de desenvolvimento, servidor de integração, produção. Deixa-se de dizer “instale estas sete coisas e depois execute” para dizer “execute este artefato”.
Esta primeira lição contém propositalmente quase nenhuma configuração. Ela estabelece o vocabulário e o modelo mental — imagem, contêiner, camada, registry — porque a quase totalidade dos bloqueios que os iniciantes encontram com o Docker vem de uma confusão sobre essas quatro palavras, e não de uma opção de comando escrita errado.
Contêiner ou máquina virtual: onde está a verdadeira diferença
A analogia “um contêiner é uma pequena VM” é a mais difundida e a mais custosa. Ela está errada justamente no ponto que importa: o que é virtualizado.
Uma máquina virtual virtualiza hardware. Um hipervisor apresenta ao convidado um processador, memória e discos simulados, e esse convidado inicializa um kernel completo, seu próprio sistema operacional, seus serviços de inicialização. Duas VMs Linux no mesmo host executam dois kernels Linux distintos.
Um contêiner não virtualiza nada disso. Ele usa o kernel do host e se limita a pedir a ele uma visão restrita do sistema, graças a dois mecanismos do kernel Linux:
- os namespaces, que isolam o que o processo enxerga — sua própria árvore de processos (ele se considera o PID 1), sua própria pilha de rede, seus próprios pontos de montagem, seu próprio hostname;
- os cgroups (control groups), que limitam o que o processo consome — cota de CPU, teto de memória, taxa de entrada/saída.
Um contêiner é, portanto, um processo comum do host, isolado e limitado. Isso explica todas as diferenças de comportamento em relação a uma VM:
| Máquina virtual | Contêiner | |
|---|---|---|
| O que é virtualizado | O hardware | A visão do sistema (namespaces) |
| Kernel | Um kernel por VM | O do host, compartilhado |
| Inicialização | Dezenas de segundos (boot do SO) | Milissegundos a alguns segundos |
| Tamanho em disco | Vários gigabytes | Alguns megabytes a centenas de MB |
| Fronteira de segurança | Forte (hipervisor) | Mais tênue (kernel compartilhado) |
| Sistemas que podem ser hospedados | Qualquer SO | Os compatíveis com o kernel do host |
As duas últimas linhas merecem ser memorizadas, e não apenas lidas por alto. Como o kernel é compartilhado, uma falha de escape de contêiner dá acesso ao host: para a execução de código não confiável e multi-inquilino, a VM continua sendo a fronteira de referência, e a indústria geralmente combina as duas (contêineres dentro de VMs). E, como o kernel é compartilhado, uma imagem Linux precisa de um kernel Linux. Quando você executa um contêiner Linux no macOS ou no Windows, o Docker Desktop executa discretamente uma máquina virtual Linux para você: o kernel vem dela.
Você mesmo pode verificar o compartilhamento do kernel, depois de instalar o Docker:
# Version du noyau de votre hôte
uname -r
# Version du noyau vue depuis l'interieur d'un conteneur Ubuntu.
# Le resultat est identique a la ligne du dessus : c'est le meme noyau.
docker run --rm ubuntu:24.04 uname -r
Já a distribuição, em contrapartida, é diferente: é a imagem que a fornece.
# L'hote est peut-etre une Debian, le conteneur se presente en Ubuntu 24.04
docker run --rm ubuntu:24.04 cat /etc/os-release
Uma imagem, portanto, não contém um sistema operacional completo — sem kernel, sem drivers. Ela contém o espaço de usuário de uma distribuição: suas bibliotecas, seus binários, sua árvore de arquivos. Daí a sua leveza.
Imagem, contêiner, registry: três objetos que não devem ser confundidos
Três palavras, três naturezas diferentes. A confusão entre as duas primeiras é, de longe, a principal causa de incompreensão.
Uma imagem é um modelo somente leitura: uma pilha de arquivos imutável, mais metadados (comando a executar, porta exposta, variáveis de ambiente, usuário). Uma imagem não é executada; ela nunca “roda”. É o equivalente a um executável no disco, ou a uma classe na programação orientada a objetos.
Um contêiner é uma instância em execução de uma imagem. O Docker pega a imagem, adiciona a ela uma fina camada gravável e inicia o processo declarado. É o equivalente a um processo em execução, ou a um objeto instanciado. Uma mesma imagem pode dar origem a cem contêineres independentes.
Um registry é um repositório de imagens, acessível pela rede. O Docker Hub é o registry público padrão; existem também os privados (GitLab, GitHub, AWS ECR, Harbor). É o equivalente a um repositório de pacotes como npm ou PyPI, mas para ambientes completos.
# 1. Recuperer une image depuis un registre vers le cache local
docker pull nginx:1.27-alpine
# 2. Lister les images presentes localement
docker images
# 3. Instancier DEUX conteneurs a partir de la MEME image,
# sur deux ports differents de l'hote
docker run -d --name web1 -p 8081:80 nginx:1.27-alpine
docker run -d --name web2 -p 8082:80 nginx:1.27-alpine
# 4. Les voir tourner cote a cote
docker ps
Dois contêineres, uma única imagem, nenhuma duplicação de arquivos no disco. Modificar um arquivo em web1 não afeta web2 nem a imagem: a escrita vai para a camada gravável própria de web1 e desaparece junto com ele.
docker exec web1 sh -c 'echo "modifie" > /usr/share/nginx/html/index.html'
# web1 a change...
docker exec web1 cat /usr/share/nginx/html/index.html
# ...web2 n'a rien vu.
docker exec web2 cat /usr/share/nginx/html/index.html
Esse detalhe é fundamental: um contêiner é efêmero por concepção. Tudo o que ele escreve em seu próprio sistema de arquivos morre com ele. Não é uma limitação a contornar, é a propriedade que torna os contêineres reproduzíveis — e é a razão de ser dos volumes, que a lição 5 trata em detalhes.
A referência de uma imagem: registry, repositório, tag
Um nome de imagem como nginx:1.27-alpine é uma forma abreviada. A forma completa é:
[registre/][espace-de-noms/]depot[:tag]
docker.io/library/nginx:1.27-alpine
│ │ │ │
│ │ │ └── tag : la version
│ │ └──────── depot
│ └──────────────── espace de noms (library = images officielles)
└────────────────────────── registre (docker.io par defaut)
Três armadilhas merecem ser apontadas desde já.
A tag latest não é uma promessa de atualização: é uma tag padrão como qualquer outra, a usada quando você não especifica nenhuma. Ela é móvel: a imagem por trás de latest muda com o tempo. Dois docker pull com seis meses de intervalo não trazem o mesmo conteúdo, o que arruína a reprodutibilidade desejada.
O sufixo -alpine designa imagens baseadas no Alpine Linux, muito pequenas porque usam a biblioteca C musl em vez da glibc. Essa economia tem um custo real: certos binários pré-compilados esperam a glibc e falham. As variantes -slim (base Debian reduzida) costumam ser o melhor meio-termo.
Por fim, para travar totalmente uma imagem, é possível referenciá-la pelo seu digest, a impressão digital criptográfica de seu conteúdo, que nunca muda:
# Un tag est mobile ; un digest est immuable
docker pull nginx@sha256:0c86dddac19f2ce4fb916fcd7015e1272ea67e3a3fbb6f88df3eb0e1c9a5c1d1
# Afficher le digest des images locales
docker images --digests
A arquitetura: cliente, daemon, registry
Quando você digita docker run, você não inicia o contêiner. Você executa um cliente de linha de comando, que envia uma requisição HTTP a uma API REST exposta por um daemon — dockerd — que faz o trabalho de fato: resolver a imagem, baixá-la se necessário, criar os namespaces e os cgroups, iniciar o processo.
docker CLI ──── API REST (socket UNIX) ────> dockerd ────> conteneurs
(votre shell) /var/run/docker.sock (le daemon) (processus isoles)
│
└──── HTTPS ────> registre
Essa separação não é um detalhe de implementação, ela tem três consequências práticas.
Primeiro, quase todos os erros do tipo “Cannot connect to the Docker daemon” não são erros de comando: o daemon não foi iniciado, ou o seu usuário não tem permissão para escrever no socket. A próxima lição resolve esse ponto.
Em seguida, o cliente pode controlar um daemon remoto. É isso que permite construir uma imagem em um servidor potente a partir de um notebook:
# Piloter un daemon distant via SSH, sans rien installer de plus
docker --host ssh://deploy@build-01.exemple.com ps
# Ou en fixant le contexte de facon persistante
docker context create serveur --docker "host=ssh://deploy@build-01.exemple.com"
docker context use serveur
docker context ls # affiche le contexte actif
docker context use default
Por fim, é preciso saber que o daemon roda com privilégios de superusuário. Dar a uma conta acesso ao socket do Docker equivale, na prática, a dar a ela acesso root ao host — um ponto a ter em mente em qualquer servidor compartilhado.
Visão geral da instalação em um comando:
# Versions du client et du daemon, systeme de stockage, nombre de conteneurs
docker version
docker info
O sistema de camadas: por que as imagens são leves e rápidas
Uma imagem não é um grande arquivo monolítico. É uma pilha de camadas (layers) empilhadas e mescladas na leitura por um sistema de arquivos union — overlay2 no Linux. Cada instrução de construção produz uma camada que registra apenas a sua diferença em relação à anterior.
# Afficher l'historique en couches d'une image, taille par couche
docker history nginx:1.27-alpine
Duas propriedades decorrem disso, e elas explicam metade dos ganhos de desempenho do Docker.
As camadas são compartilhadas entre imagens. Se dez das suas imagens derivam de python:3.12-slim, essa base é armazenada apenas uma vez no disco e baixada apenas uma vez. Você pode observar isso diretamente: o primeiro pull de uma família de imagens é demorado, e os seguintes mostram Already exists na maioria das camadas.
docker pull python:3.12-slim
# Les couches communes s'affichent en "Already exists", pas retelechargees
docker pull python:3.12
As camadas são imutáveis e endereçadas por conteúdo. Uma camada inalterada é reutilizada a partir do cache em vez de ser reconstruída. Essa é a principal alavanca de otimização dos Dockerfiles — assunto da lição 3 — e a razão pela qual a ordem das instruções em um Dockerfile muda radicalmente o tempo de build.
As camadas também são aditivas, nunca subtrativas: remover um arquivo em uma camada posterior o oculta, mas não o retira da imagem. Um segredo copiado e depois “removido” continua podendo ser extraído da imagem. Voltaremos a isso.
O que o Docker não faz
Estabelecer os limites desde já evita expectativas frustradas.
O Docker não orquestra um parque de máquinas. Ele gerencia os contêineres de um host. Distribuir contêineres por vinte servidores, reiniciá-los em outro lugar quando uma máquina cai, escalar automaticamente: esse é o trabalho de um orquestrador como o Kubernetes. O Docker Compose, visto na lição 4, coordena vários contêineres em um único host.
O Docker não torna uma aplicação escalável ou stateless. Conteinerizar um software que grava em uma pasta local e mantém suas sessões em memória resulta em um contêiner que grava em uma pasta local e mantém suas sessões em memória. O Docker torna essas escolhas de arquitetura visíveis; ele não as corrige.
O Docker não é uma sandbox de segurança absoluta. Por padrão, um contêiner roda como root (dentro do seu namespace), e várias opções comuns — --privileged, a montagem do socket do Docker — escancaram a porta para o host.
Por fim, o Docker não é mais a única implementação. O formato das imagens e o comportamento de execução são padronizados pela Open Container Initiative. Ferramentas como Podman ou containerd consomem as mesmas imagens. O que você aprende aqui, portanto, não está preso a um produto.
O ciclo de trabalho completo, em visão geral
Antes de entrar nos comandos, aqui está o ciclo que as cinco lições seguintes vão desenvolver:
Dockerfile ──build──> Image ──push──> Registre
│ │
│ pull
run │
v v
Conteneur <──────── Serveur de production
Você escreve um Dockerfile (lição 3), constrói uma imagem a partir dele, a executa localmente para verificar, a envia para um registry, e um servidor a baixa para executá-la de forma idêntica (lição 6). Nesse meio-tempo, o Compose (lição 4) permite que você faça vários contêineres conviverem, conectados por uma rede e apoiados em volumes (lição 5).
Aqui está esse ciclo reduzido aos seus comandos essenciais, como referência — cada um será retomado em detalhes:
docker build -t mon-app:1.0 . # Dockerfile -> image
docker run -d -p 8080:8080 mon-app:1.0 # image -> conteneur
docker push registre.exemple.com/mon-app:1.0 # image -> registre
Pontos-chave
- Um contêiner não é uma pequena VM: é um processo do host isolado pelos namespaces e limitado pelos cgroups, que usa o kernel do host. Daí sua leveza, sua velocidade de inicialização e uma fronteira de segurança mais fina que a de um hypervisor.
- Imagem = modelo imutável somente leitura; contêiner = instância em execução desse modelo; registry = repositório de imagens. Uma imagem não é executada, e um contêiner não é compartilhado.
- Tudo o que um contêiner grava em seu sistema de arquivos desaparece junto com ele. É intencional, e é a razão de ser dos volumes.
- Uma imagem é uma pilha de camadas imutáveis compartilhadas entre imagens e mantidas em cache: a principal alavanca para builds rápidos e transferências leves.
- O cliente
dockernão faz nada sozinho: ele conversa com o daemondockerdpor meio de uma API. Esse desacoplamento explica os erros de conexão ao socket e permite controlar um host remoto. - Evite a tag
latestsempre que a reprodutibilidade for importante: fixe uma versão ou um digestsha256:para um travamento total.
Ponto de referência: a documentação oficial está em docs.docker.com. Ela é a referência para as opções de comandos, que evoluem de uma versão para outra.
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão