Containers - Docker e Docker Compose
Imagem x container
Seção intitulada “Imagem x container”- Imagem — artefato imutável, versionado por tags, composto de camadas somente-leitura empilhadas. É o “template”.
- Container — instância em execução de uma imagem, com uma fina camada de escrita no topo (copy-on-write). É efêmero: o que grava dentro dele some quando é removido, salvo se persistido em volume.
Uma imagem é a classe; o container é o objeto. Da mesma imagem você instancia N containers idênticos.
Camadas e cache de build
Seção intitulada “Camadas e cache de build”Cada instrução do Dockerfile (FROM, COPY, RUN…) gera uma camada. O Docker cacheia camadas: se nada mudou naquele passo e nos anteriores, ele reaproveita a camada em vez de reconstruí-la. A ordem das instruções, portanto, é decisão de performance:
- Copie primeiro o que muda pouco (arquivos de dependências:
pom.xml,build.gradle) e resolva as dependências. Só depois copie o código-fonte, que muda a cada commit. - Assim, alterar uma classe não invalida o cache do download de dependências — o build fica muito mais rápido.
Build multi-stage
Seção intitulada “Build multi-stage”O padrão essencial para Java. Um estágio pesado com o JDK + Maven/Gradle compila o jar; um estágio final leve, só com a JRE slim, recebe apenas o artefato. O toolchain de build não vaza para a imagem de produção — imagem menor, superfície de ataque menor.
Dockerfile multi-stage para Spring Boot
Seção intitulada “Dockerfile multi-stage para Spring Boot”# ---- Estágio 1: build ----FROM eclipse-temurin:21-jdk AS buildWORKDIR /app
# Copia só o necessário para resolver dependências (bom para o cache de camadas)COPY .mvn/ .mvn/COPY mvnw pom.xml ./RUN ./mvnw dependency:go-offline -B
# Agora o código-fonte (muda a cada commit)COPY src ./srcRUN ./mvnw clean package -DskipTests -B
# ---- Estágio 2: runtime ----FROM eclipse-temurin:21-jre-alpine AS runtimeWORKDIR /app
# Usuário não-root: nunca rode a aplicação como rootRUN addgroup -S spring && adduser -S spring -G springUSER spring
# Copia apenas o jar do estágio de buildCOPY --from=build /app/target/*.jar app.jar
EXPOSE 8080ENTRYPOINT ["java", "-jar", "app.jar"].dockerignore
Seção intitulada “.dockerignore”Sem ele, COPY . . arrasta target/, .git/, IDE, etc. para o contexto de build — lento e potencialmente vazando artefatos locais.
target/.git/.gitignore.idea/*.imlDockerfiledocker-compose.ymlREADME.mdDocker Compose para dev local (app + Postgres)
Seção intitulada “Docker Compose para dev local (app + Postgres)”Compose descreve, em um YAML, um conjunto de serviços que sobem juntos com uma rede compartilhada. Ideal para levantar app + banco em um comando (docker compose up).
services: db: image: postgres:16-alpine environment: POSTGRES_DB: appdb POSTGRES_USER: app POSTGRES_PASSWORD: secret ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: # App só deve subir quando o banco aceitar conexões test: ["CMD-SHELL", "pg_isready -U app -d appdb"] interval: 5s timeout: 3s retries: 5
app: build: . ports: - "8080:8080" environment: # 'db' é o hostname na rede do compose (nome do serviço) SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/appdb SPRING_DATASOURCE_USERNAME: app SPRING_DATASOURCE_PASSWORD: secret depends_on: db: condition: service_healthy
volumes: pgdata:Pontos-chave do exemplo: o app alcança o banco pelo nome do serviço (db) via DNS interno da rede do Compose; depends_on com condition: service_healthy garante ordem de inicialização; o volume nomeado pgdata persiste os dados entre up/down.
Ponto-chave: containers e testes de integração
Seção intitulada “Ponto-chave: containers e testes de integração”Aqui a área de containers encontra a de testes. Testcontainers é uma biblioteca que sobe dependências reais em containers Docker efêmeros durante a execução da suíte — um Postgres de verdade, um Kafka de verdade — e os derruba ao final.
- Isso eleva a fidelidade dos testes de integração (Tipos de Teste - Unit, Integration e E2E): você valida contra o banco real (dialetos SQL, migrations, constraints), não contra um H2 que se comporta diferente.
- Diferentemente de um mock (que substitui a colaboração), Testcontainers fornece a dependência real e descartável. Por isso depende do runtime Docker estar disponível — inclusive no runner da CI - GitHub Actions.
- Vantagem sobre o Compose para testes: o ciclo de vida é gerenciado pelo próprio teste (portas aleatórias, isolamento entre execuções), evitando estado compartilhado e “testes que sujam o ambiente”.
graph TD
A[Dockerfile] --> B[docker build]
B --> C[Imagem: camadas read-only]
C --> D[docker run]
D --> E[Container em execução]
C --> F[docker push]
F --> G[Registry]
subgraph "Dev local / CI"
H[docker compose up] --> I[app]
H --> J[Postgres]
K[Testcontainers] --> L[Postgres efêmero p/ testes]
end
Boas práticas de imagem
Seção intitulada “Boas práticas de imagem”- Tamanho — base slim/alpine, multi-stage, sem toolchain de build no runtime; menos MB = pull/deploy mais rápido e menos superfície de ataque.
- Usuário não-root — crie e use um usuário dedicado; container comprometido rodando como root é risco desnecessário.
- Layer caching — ordene instruções da menos volátil para a mais volátil (dependências antes do código).
- Imutabilidade e tags — evite
latestem produção; fixe versões para builds reproduzíveis. - Um processo por container — o container encapsula um serviço; orquestre múltiplos serviços via Compose/orquestrador, não empilhando processos dentro de um container.
- Configuração via ambiente — nunca embuta segredos na imagem; injete por variáveis de ambiente/secrets.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”FROM openjdk:latest(JDK completo em produção). Imagem gigante com toolchain desnecessário; use JRE slim em multi-stage.COPY . .sem.dockerignore. Contexto de build inchado, cache invalidado à toa e risco de vazar arquivos locais.- Rodar como root. Padrão inseguro; sempre defina
USER. - Instruções voláteis no topo do Dockerfile. Copiar o código antes de resolver dependências destrói o cache a cada commit.
- Persistir dados na camada de escrita do container. Ao remover o container, os dados somem; use volumes.
- Segredos como
ENV/ARGfixos na imagem. Ficam gravados nas camadas e visíveis no histórico da imagem. latestem produção. Deploys não reproduzíveis; a mesma tag pode apontar para builds diferentes.
Relações
Seção intitulada “Relações”- Executa no pipeline: CI - GitHub Actions constrói e publica a imagem
- Área geral: DevOps & Infraestrutura
- Onde a imagem roda: AWS - Visão Geral dos Serviços · Tipos de Infraestrutura
- Testes reais e descartáveis: Testcontainers · Test Doubles - Stub, Spy, Mock e Fake
- Deploy independente por serviço: Microservices
- Índice: Home
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. Qual a diferença entre imagem e container, e o que acontece com dados gravados dentro de um container?
Resposta
A imagem é um artefato imutável em camadas somente-leitura (o template); o container é uma instância em execução com uma fina camada de escrita copy-on-write no topo. Dados gravados nessa camada são efêmeros: somem quando o container é removido, a menos que sejam persistidos em um volume.
2. Por que um build multi-stage é o padrão para empacotar apps Spring Boot?
Resposta
Porque separa o estágio de compilação (JDK + Maven/Gradle, pesado) do estágio de runtime (JRE slim). Só o jar final é copiado para a imagem de produção, que fica pequena e sem o toolchain de build — menos MB, deploy mais rápido e menor superfície de ataque.
3. Como a ordem das instruções no Dockerfile afeta o cache de camadas?
Resposta
Cada instrução vira uma camada cacheável; uma camada é reaproveitada se ela e todas as anteriores não mudaram. Copiando primeiro os arquivos de dependências (pom.xml) e resolvendo-as, e só depois o código-fonte, uma alteração de classe não invalida o cache do download de dependências, acelerando muito o build.
4. Por que Testcontainers depende de Docker e que vantagem traz sobre um banco em memória?
Resposta
Testcontainers sobe dependências reais em containers Docker efêmeros durante os testes, exigindo o runtime Docker disponível (inclusive no runner da CI). A vantagem sobre um H2 em memória é a fidelidade: você testa contra o Postgres real — mesmos dialetos SQL, migrations e constraints — reduzindo a chance de o teste passar e a produção falhar.