DevOps, CI/CD e contêineres
Fundamentos de Docker: imagens, containers e Dockerfiles
Iniciante7 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O Docker empacota uma aplicação com tudo de que ela precisa em uma imagem e executa essa imagem como um processo isolado chamado container. Este guia explica as peças — imagens, camadas, registries, containers, volumes — e depois mostra como escrever um Dockerfile que faz o build rápido e gera uma imagem pequena e segura, com exemplos práticos para um serviço Spring Boot.
O que você vai aprender
- Explicar a diferença entre imagem, camada, container e registry
- Executar, inspecionar, parar e remover containers com os comandos essenciais da CLI
- Escrever um Dockerfile multi-stage que gere uma imagem pequena e reproduzível
- Usar .dockerignore, a ordem das camadas e cache mounts para manter os builds rápidos
- Evitar os erros mais comuns: rodar como root, usar a tag latest, colocar segredos na imagem
“Na minha máquina funciona” já foi piada; o Docker transformou isso em estratégia de deploy ao tornar a máquina parte do artefato. Uma imagem carrega a aplicação, o runtime, as bibliotecas e a configuração. Rode a mesma imagem em um notebook, em um runner de CI e em um cluster de produção e você terá o mesmo comportamento. Este guia explica o modelo por trás dessa promessa e depois parte para a prática: os comandos que você usa todo dia e como escrever um Dockerfile do qual não vai se envergonhar depois.
Imagens, camadas, containers e registries
Quatro palavras concentram a maior parte do vocabulário do Docker.
Uma imagem é um template somente leitura: um snapshot de sistema de arquivos mais metadados, como o comando padrão, as variáveis de ambiente e as portas expostas. Imagens são construídas a partir de um Dockerfile e identificadas por um nome e uma tag, por exemplo eclipse-temurin:21-jre.
Uma imagem é formada por camadas (layers). Cada instrução de um Dockerfile que altera o sistema de arquivos — COPY, RUN, ADD — gera uma nova camada empilhada sobre as anteriores. As camadas são endereçadas pelo conteúdo e compartilhadas: se dez imagens começam todas com FROM eclipse-temurin:21-jre, as camadas base são armazenadas e baixadas uma única vez. É também isso que faz o cache de build funcionar, como veremos.
Um container é uma instância de uma imagem, em execução ou parada. O Docker adiciona uma fina camada gravável sobre as camadas somente leitura da imagem, cria uma visão isolada da tabela de processos, da rede e do sistema de arquivos usando namespaces do Linux e aplica limites de recursos com cgroups. O container é só um processo no kernel do host; nenhum sistema operacional convidado é inicializado.
Um registry armazena e distribui imagens. O Docker Hub é o registry público padrão; as organizações mantêm registries privados (Harbor, GitLab, Amazon ECR, GitHub Container Registry). docker pull e docker push movem imagens entre um host e um registry.
$ docker pull eclipse-temurin:21-jre
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
eclipse-temurin 21-jre 3a1f0b2c9d7e 2 weeks ago 268MB
Os comandos do dia a dia
Dá para ir longe com uma dúzia de comandos.
# Rodar um container a partir de uma imagem, em modo interativo, e removê-lo ao sair
docker run --rm -it ubuntu:24.04 bash
# Rodar um servidor web em segundo plano, mapeando a porta 8080 do host para a porta 80 do container
docker run -d --name web -p 8080:80 nginx:1.27
# Ver o que está rodando (adicione -a para incluir também os containers parados)
docker ps
# Logs, um shell dentro do container, uso de recursos
docker logs -f web
docker exec -it web sh
docker stats web
# Parar, iniciar, remover
docker stop web
docker start web
docker rm -f web
# Inspecionar qualquer coisa — imagens, containers, redes — em JSON
docker inspect web | jq '.[0].NetworkSettings.Ports'
# Liberar disco: containers parados, imagens órfãs, redes sem uso
docker system prune
Vale internalizar duas flags logo no começo. --rm apaga o container quando ele termina, o que evita que sua máquina fique cheia de containers mortos dos seus experimentos. -v (ou --mount) anexa um volume ou um diretório do host para que os dados sobrevivam ao container:
docker run -d --name db -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=dev postgres:16
Sem o volume, os arquivos do banco ficam na camada gravável do container e somem com docker rm.
Escrevendo um Dockerfile
Um Dockerfile é um script de instruções que produzem uma imagem. Veja um exemplo mínimo para uma aplicação Spring Boot cujo build já gerou target/app.jar:
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Faça o build e rode:
docker build -t myorg/app:1.0.0 .
docker run --rm -p 8080:8080 myorg/app:1.0.0
O . no final é o contexto de build — o diretório enviado ao daemon do Docker. Tudo o que o COPY referencia precisa estar dentro dele, e tudo o que estiver dentro dele é transferido; por isso o .dockerignore importa.
As instruções que você vai usar:
| Instrução | Finalidade |
|---|---|
FROM | Imagem base. Todo Dockerfile começa aqui (ou com FROM scratch para partir de uma base vazia). |
WORKDIR | Define o diretório de trabalho das instruções seguintes; cria o diretório se ele não existir. |
COPY | Copia arquivos do contexto de build para a imagem. Prefira-a ao ADD, que também baixa URLs e descompacta arquivos compactados. |
RUN | Executa um comando em tempo de build e grava o resultado como uma camada. |
ENV | Define uma variável de ambiente para o build e para a execução. |
ARG | Variável de build, passada com --build-arg; não existe em tempo de execução. |
EXPOSE | Documenta em qual porta o processo escuta. Não publica nada. |
USER | Troca o usuário das instruções seguintes e do container em execução. |
ENTRYPOINT / CMD | O comando a executar. Use a forma de array JSON para que nenhum shell fique no meio e os sinais cheguem ao processo. |
HEALTHCHECK | Um comando que o Docker roda periodicamente para informar a saúde do container. |
Multi-stage builds
O Dockerfile acima pressupõe que o jar foi gerado fora do Docker, o que deixa o build dependente do Maven e do JDK do host. Um multi-stage build faz a compilação dentro de um primeiro estágio e copia só o artefato para um estágio de runtime enxuto:
# ---- estágio de build ----
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
# Copiar primeiro os arquivos que menos mudam, para que o download das dependências fique em cache
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -q dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -q package -DskipTests
# ---- estágio de runtime ----
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 10001 app
WORKDIR /app
COPY --from=build /src/target/app.jar app.jar
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
O estágio de build, com o Maven e o JDK completo, é descartado; a imagem final contém só o JRE e o jar. O tamanho cai de cerca de 700 MB para menos de 300 MB, e nem o código-fonte nem as ferramentas de build vão para produção. O mesmo padrão vale para Node (build com node:22, execução com node:22-slim ou arquivos estáticos servidos pelo nginx), Go (build e depois FROM scratch) e todo o resto.
Builds rápidos: cache e ordem das instruções
O Docker guarda cada camada em cache e a reutiliza enquanto a instrução e suas entradas não mudam. O cache é invalidado na primeira instrução cujas entradas mudaram, e todas as instruções seguintes são refeitas. Duas consequências:
- Ordene as instruções da menos para a mais volátil. Copie o
pom.xmlou opackage.jsone instale as dependências antes de copiar o código-fonte. Assim, editar um arquivo-fonte refaz só a etapa de compilação, e não o download das dependências. - Use
.dockerignore. UmCOPY . .é invalidado sempre que qualquer arquivo do contexto muda — inclusive.git, a saída do build e os arquivos temporários do editor. Exclua-os:
.git
target
node_modules
*.log
.idea
.vscode
Dockerfile
docker-compose*.yml
O --mount=type=cache do BuildKit (mostrado acima) mantém os caches do gerenciador de pacotes entre builds sem gravá-los em uma camada. Junte com && os comandos RUN que andam juntos, para que os arquivos intermediários nunca virem uma camada, e faça a limpeza na mesma instrução:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
O essencial de segurança e higiene
Não rode como root. Por padrão, o processo dentro de um container roda com UID 0. Um escape do container ou um bug da aplicação entrega então a um atacante acesso root ao sistema de arquivos do host que o container enxerga. Crie um usuário e troque para ele com USER, como no exemplo multi-stage.
Fixe as imagens base. FROM node ou FROM node:latest significa uma imagem diferente a cada semana. Use uma tag específica (node:22.9-slim) e, para builds reproduzíveis, um digest: FROM node:22.9-slim@sha256:…. Refaça o build de forma deliberada para pegar os patches de segurança.
Nunca coloque segredos em uma imagem. Um COPY .env . ou um ENV API_KEY=... fica gravado para sempre em uma camada, visível com docker history, e vai para todo registry que a imagem alcançar. Passe os segredos em tempo de execução por variáveis de ambiente ou arquivos montados, e use o --mount=type=secret do BuildKit se uma etapa do build realmente precisar de um.
Mantenha a imagem pequena. Menos pacotes significam menos vulnerabilidades para corrigir e pulls mais rápidos. Prefira bases -slim, -alpine ou distroless, e escaneie as imagens com docker scout ou Trivy na CI.
Trate os sinais. Use a forma de array JSON do ENTRYPOINT para que seu processo seja o PID 1 e receba o SIGTERM quando o container parar. Com a forma shell, ENTRYPOINT java -jar app.jar, o sh fica na frente do seu processo e engole o sinal: sua aplicação é morta sem um shutdown gracioso quando terminam os dez segundos de tolerância.
Erros comuns
- Tratar containers como bichinhos de estimação. Entrar em um container e editar arquivos lá dentro funciona até o container ser recriado. Altere o Dockerfile e refaça o build.
- Publicar portas só com
EXPOSE.EXPOSEé documentação. Quem publica de fato é o-p host:container(ou uma entradaportsno Compose). - Achar que
localhosté o host. Dentro de um container,localhosté o próprio container. Acesse outros containers por uma rede definida pelo usuário e pelos nomes de serviço, e o host porhost.docker.internalno Docker Desktop. - Gravar dados no sistema de arquivos do container. Eles somem no
docker rm. Use volumes. - Ignorar a regra de um processo por container. Enfiar uma aplicação, um banco de dados e um daemon de cron em uma única imagem atrapalha a orquestração, os logs e a escalabilidade. Compose e Kubernetes existem para aplicações com vários containers.
- Ignorar o código de saída.
docker ps -amostra os containers encerrados com seus códigos;docker logsmostra o motivo.137significa que o processo foi morto pelo OOM killer ou por umSIGKILL: aumente o limite de memória ou corrija o vazamento.
Principais conclusões
- Uma imagem é um template em camadas, somente leitura; um container é um processo rodando a partir dela com uma camada gravável por cima; um registry armazena imagens.
- Aprenda
run,ps,logs,exec,stop,rm,build,pushesystem prune. Use--rmnos experimentos e-vem tudo que guarda dados. - Escreva Dockerfiles multi-stage: faça o build em uma imagem completa, rode em uma enxuta e copie só o artefato de uma para a outra.
- Ordene as instruções da mais estável para a mais volátil, mantenha o
.dockerignoree use cache mounts para que uma mudança no código-fonte refaça só a etapa de compilação. - Rode com um usuário que não seja root, fixe as imagens base, mantenha os segredos fora das camadas, use a forma JSON do
ENTRYPOINTe mantenha a imagem pequena.
Fontes e leituras complementares
Perguntas frequentes
- Um container é uma máquina virtual leve?
- Não. Uma máquina virtual roda um sistema operacional convidado completo sobre um hipervisor. Um container é um processo comum no kernel do host, isolado com namespaces do Linux e limitado com cgroups, que enxerga o próprio sistema de arquivos vindo da imagem. É por isso que containers sobem em milissegundos e compartilham o kernel do host — e por isso um container não consegue rodar um kernel diferente do host.
- Qual é a diferença entre CMD e ENTRYPOINT?
- ENTRYPOINT define o executável que sempre roda; CMD fornece argumentos padrão que o usuário pode substituir na linha de comando do docker run. Use ENTRYPOINT para a parte fixa (o comando java e suas flags) e CMD para os argumentos que você espera que as pessoas mudem. Se só o CMD estiver definido, o comando inteiro pode ser substituído.
- Por que minha imagem está tão grande?
- Normalmente porque as ferramentas de build, o código-fonte e os caches do gerenciador de pacotes foram parar na imagem final. Um multi-stage build compila em um estágio e copia só o artefato resultante para uma imagem de runtime enxuta. Confira também se o .dockerignore exclui .git, node_modules, target e as pastas da IDE, e se você limpa os caches do gerenciador de pacotes na mesma instrução RUN que instala os pacotes.
- Devo usar a tag latest?
- Não para nada que você faça deploy. latest é só uma tag mutável que aponta para o que foi enviado por último, então dois builds em dias diferentes podem gerar imagens diferentes. Fixe as imagens base em uma versão específica (de preferência em um digest) e marque suas próprias imagens com a versão do build ou o hash do commit.