Docker & Kubernetes

O que Todo Dev Deve Saber sobre Dockerfile em Profundidade: Cada Instrução e seu Impacto no Build

19 min de leitura

O que Todo Dev Deve Saber sobre Dockerfile em Profundidade: Cada Instrução e seu Impacto no Build

Introdução: Por que entender Dockerfile é fundamental Um Dockerfile é um script declarativo que define como construir uma imagem Docker — o template imutável que permite criar contêineres consistentes em qualquer ambiente. A maioria dos desenvolvedores trata Dockerfiles como um "set and forget", copiam exemplos da internet e seguem adiante. Isso funciona até o momento em que você precisa otimizar tempo de build, reduzir tamanho de imagem ou debugar um comportamento inesperado em produção. Neste artigo, vamos explorar cada instrução principal de um Dockerfile, entender por que cada uma existe, como elas impactam o processo de build e, mais importante, como suas decisões afetam performance, segurança e manutenibilidade. Você aprenderá a escrever Dockerfiles que não apenas funcionam, mas que são eficientes e profissionais. Fundamentos: A Estrutura e o Fluxo de Construção O que acontece durante um Docker Build Quando você executa , o Docker passa por um processo bem definido: lê o Dockerfile linha por linha, executa cada instrução em

<h2>Introdução: Por que entender Dockerfile é fundamental</h2>

<p>Um Dockerfile é um script declarativo que define como construir uma imagem Docker — o template imutável que permite criar contêineres consistentes em qualquer ambiente. A maioria dos desenvolvedores trata Dockerfiles como um &quot;set and forget&quot;, copiam exemplos da internet e seguem adiante. Isso funciona até o momento em que você precisa otimizar tempo de build, reduzir tamanho de imagem ou debugar um comportamento inesperado em produção.</p>

<p>Neste artigo, vamos explorar cada instrução principal de um Dockerfile, entender <em>por que</em> cada uma existe, como elas impactam o processo de build e, mais importante, como suas decisões afetam performance, segurança e manutenibilidade. Você aprenderá a escrever Dockerfiles que não apenas funcionam, mas que são eficientes e profissionais.</p>

<h2>Fundamentos: A Estrutura e o Fluxo de Construção</h2>

<h3>O que acontece durante um Docker Build</h3>

<p>Quando você executa <code>docker build -t minha-app:1.0 .</code>, o Docker passa por um processo bem definido: lê o Dockerfile linha por linha, executa cada instrução em um contêiner temporário, salva o resultado como uma camada (layer), descarta o contêiner temporário e usa a camada anterior como base para a próxima instrução. Esse sistema de camadas é a chave para entender Dockerfiles.</p>

<p>Cada instrução cria uma nova camada. Se a instrução não mudou desde o último build, o Docker reutiliza a camada em cache. Isso significa que a <em>ordem das instruções importa profundamente</em> — instruções que mudam frequentemente devem vir por último, não por primeiro. A estrutura de um Dockerfile segue um fluxo lógico: especificar a imagem base, instalar dependências, copiar código, executar testes ou build, e finalmente definir como iniciar o contêiner.</p>

<h2>FROM, WORKDIR e as Instruções Essenciais</h2>

<h3>FROM: Escolhendo a base certa</h3>

<p>A instrução <code>FROM</code> especifica a imagem base. Essa escolha é crítica — ela determina o sistema operacional, as ferramentas pré-instaladas, o tamanho inicial da imagem e, consequentemente, a superfície de ataque de segurança.</p>

<pre><code class="language-dockerfile">FROM ubuntu:22.04</code></pre>

<p>Este Dockerfile constrói sobre Ubuntu completo (cerca de 77MB já da base). Se você apenas precisa rodar uma aplicação Python, isso é desperdício. Imagens especializadas como <code>python:3.11-slim</code> (150MB) ou até melhor, <code>python:3.11-alpine</code> (50MB) reduzem significativamente o tamanho. Alpine usa musl libc ao invés de glibc, o que causa incompatibilidades ocasionais com bibliotecas compiladas, mas geralmente é a escolha ideal para contêineres.</p>

<pre><code class="language-dockerfile">FROM python:3.11-alpine</code></pre>

<p>Um padrão profissional é usar tags específicas, nunca <code>latest</code>. O <code>latest</code> é um alvo móvel — sua imagem pode quebrar semanas depois quando uma atualização é lançada. Use versões:</p>

<pre><code class="language-dockerfile">FROM python:3.11.7-alpine3.18</code></pre>

<p>Se você está compilando C/C++ durante o build mas não precisa disso na execução, use builds multi-estágio (discutiremos depois).</p>

<h3>WORKDIR: Organizando o espaço de trabalho</h3>

<p><code>WORKDIR</code> define o diretório de trabalho dentro do contêiner. Todas as instruções subsequentes (RUN, COPY, ADD, CMD) operam neste contexto.</p>

<pre><code class="language-dockerfile">FROM python:3.11-alpine

WORKDIR /app</code></pre>

<p>Se <code>WORKDIR</code> não existir, o Docker o cria automaticamente. Use caminhos absolutos, nunca relativos. <code>WORKDIR /app</code> é claro; <code>WORKDIR ./src</code> cria confusão e bugs. Um padrão comum em ambientes corporativos é criar um usuário não-root e definir o WORKDIR com permissões apropriadas, que veremos mais adiante.</p>

<h3>RUN: Executando comandos</h3>

<p><code>RUN</code> executa um comando shell durante o build. Cada <code>RUN</code> cria uma camada. Iniciantes cometem o erro de fazer uma instrução <code>RUN</code> por comando:</p>

<pre><code class="language-dockerfile"># RUIM - cria 3 camadas desnecessárias

RUN apk add --no-cache python3

RUN apk add --no-cache pip

RUN pip install flask</code></pre>

<p>Combine em uma única <code>RUN</code>:</p>

<pre><code class="language-dockerfile"># BOM - cria 1 camada

RUN apk add --no-cache python3 pip &amp;&amp; \

pip install --no-cache-dir flask==2.3.0</code></pre>

<p>Note o uso de <code>--no-cache-dir</code> no pip e <code>--no-cache</code> no apk — esses flags removem caches de gerenciadores de pacotes, reduzindo o tamanho da camada. Em contêineres, você não reutiliza o gerenciador, então esses caches são lixo.</p>

<pre><code class="language-dockerfile">FROM python:3.11-alpine

WORKDIR /app

Instalação de dependências do sistema e Python

RUN apk add --no-cache \

gcc \

musl-dev \

&amp;&amp; pip install --no-cache-dir --upgrade pip

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt</code></pre>

<p>Este padrão é padrão industrial: instale dependências de sistema, depois dependências Python. As dependências de sistema raramente mudam, então sua camada é reutilizável. <code>requirements.txt</code> pode mudar frequentemente, então sua camada é reconstruída quando necessário.</p>

<h2>COPY, ADD e Gerenciamento de Arquivos</h2>

<h3>COPY: O comando correto para copiar arquivos</h3>

<p><code>COPY</code> copia arquivos do seu host para o contêiner. É simples e previsível:</p>

<pre><code class="language-dockerfile">COPY requirements.txt /app/

COPY src/ /app/src/</code></pre>

<p>Você pode usar wildcards:</p>

<pre><code class="language-dockerfile">COPY *.txt /app/</code></pre>

<p>E copiar com alteração de proprietário:</p>

<pre><code class="language-dockerfile">COPY --chown=nobody:nobody app.py /app/</code></pre>

<p>Use <code>COPY</code> na maioria dos casos. É mais claro que <code>ADD</code> e melhor para cache — o Docker invalida a camada apenas se os arquivos realmente mudarem (comparação de hash).</p>

<h3>ADD: Quando usar, e quando evitar</h3>

<p><code>ADD</code> é similar ao <code>COPY</code>, mas com recursos extras: pode extrair arquivos .tar automaticamente e suporta URLs. Essas extras introduzem complexidade:</p>

<pre><code class="language-dockerfile"># ADD baixa de URL e extrai automaticamente

ADD https://example.com/archive.tar.gz /tmp/

Equivalente mais explícito e profissional

RUN wget -O /tmp/archive.tar.gz https://example.com/archive.tar.gz &amp;&amp; \

tar -xzf /tmp/archive.tar.gz -C /tmp &amp;&amp; \

rm /tmp/archive.tar.gz</code></pre>

<p>A versão explícita com <code>RUN</code> é preferível em código corporativo — é clara, suas intenções são óbvias, e você controla exatamente o que acontece. Use <code>ADD</code> apenas para arquivos .tar quando for realmente necessário.</p>

<h3>Exemplo prático: Estrutura de projeto</h3>

<pre><code class="language-dockerfile">FROM python:3.11-alpine

WORKDIR /app

Copiar estrutura do projeto

COPY src/ ./src/

COPY tests/ ./tests/

COPY requirements.txt requirements-dev.txt ./

Instalar dependências

RUN pip install --no-cache-dir -r requirements.txt

Testes durante o build (falhar rápido)

RUN python -m pytest tests/ --tb=short

EXPOSE 8000

CMD [&quot;python&quot;, &quot;-m&quot;, &quot;uvicorn&quot;, &quot;src.main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;]</code></pre>

<p>Este Dockerfile segue um padrão profissional: copiamos estrutura, instalamos, testamos no build, e apenas se tudo passar, a imagem é criada. Se um teste falhar, o build falha, impedindo que código quebrado vire imagem.</p>

<h2>ENV, EXPOSE, ENTRYPOINT e CMD</h2>

<h3>ENV: Variáveis de ambiente</h3>

<p><code>ENV</code> define variáveis que existem tanto no build quanto no contêiner em execução:</p>

<pre><code class="language-dockerfile">ENV PYTHONUNBUFFERED=1

ENV APP_ENV=production

ENV DATABASE_URL=postgresql://localhost/mydb</code></pre>

<p><code>PYTHONUNBUFFERED=1</code> é crítico para aplicações Python em contêineres — força Python a não bufferizar stdout, garantindo que logs apareçam imediatamente. Sem isso, seus logs podem desaparecer ou atrasar em um crash.</p>

<p>Variáveis sensíveis (senhas, chaves) devem ser passadas em tempo de execução, nunca hardcodadas:</p>

<pre><code class="language-dockerfile"># RUIM - a senha fica na imagem permanentemente

ENV DB_PASSWORD=super_secret_123

BOM - usar build args ou runtime secrets</code></pre>

<p>Se precisar de uma variável apenas no build:</p>

<pre><code class="language-dockerfile">ARG BUILD_DATE

ARG VERSION=1.0.0

RUN echo &quot;Build date: $BUILD_DATE&quot; &amp;&amp; echo &quot;Version: $VERSION&quot;</code></pre>

<p><code>ARG</code> existe apenas durante o build. <code>ENV</code> persiste no contêiner.</p>

<h3>EXPOSE: Documentação de portas</h3>

<p><code>EXPOSE</code> documenta qual porta a aplicação usa, mas <em>não abre a porta</em>:</p>

<pre><code class="language-dockerfile">EXPOSE 8000</code></pre>

<p>Isso é puramente informacional. Ao rodar <code>docker run -P</code>, o Docker usa <code>EXPOSE</code> para mapear portas automaticamente. Sempre use <code>EXPOSE</code> para deixar claro qual porta sua aplicação usa, mesmo que você mapeie diferente em runtime.</p>

<h3>ENTRYPOINT vs CMD: A diferença sutil mas crítica</h3>

<p>Iniciantes confundem <code>ENTRYPOINT</code> e <code>CMD</code>. Entenda assim:</p>

<ul>

<li><code>ENTRYPOINT</code> é o executável principal do contêiner. Nunca muda (geralmente).</li>

<li><code>CMD</code> são argumentos padrão para <code>ENTRYPOINT</code>.</li>

</ul>

<pre><code class="language-dockerfile"># Exemplo 1: Apenas CMD

FROM python:3.11-alpine

CMD [&quot;python&quot;, &quot;app.py&quot;]

Rodar: docker run myimage

Executa: python app.py

Rodar: docker run myimage --debug

Executa: python app.py --debug (CMD é substituído completamente)</code></pre>

<pre><code class="language-dockerfile"># Exemplo 2: ENTRYPOINT + CMD (melhor prática)

FROM python:3.11-alpine

ENTRYPOINT [&quot;python&quot;, &quot;app.py&quot;]

CMD [&quot;--port&quot;, &quot;8000&quot;]

Rodar: docker run myimage

Executa: python app.py --port 8000

Rodar: docker run myimage --port 9000

Executa: python app.py --port 9000 (apenas CMD é substituído)</code></pre>

<p>A forma &quot;exec&quot; (array JSON) é preferível à forma shell:</p>

<pre><code class="language-dockerfile"># Ruim - exec em shell, sinais não funcionam corretamente

ENTRYPOINT &quot;python app.py&quot;

Bom - exec direto

ENTRYPOINT [&quot;python&quot;, &quot;app.py&quot;]</code></pre>

<p>Na forma shell, seu aplicativo é filho de um shell, impedindo que sinais como SIGTERM sejam recebidos corretamente. Ao parar um contêiner, ele fica esperando timeout ao invés de desligar gracefully.</p>

<h2>Otimizações e Padrões Avançados</h2>

<h3>Multi-stage builds: Separando build de runtime</h3>

<p>Um multi-stage build cria uma imagem &quot;builder&quot; temporária e depois copia apenas artefatos necessários para a imagem final, descartando ferramentas de build.</p>

<pre><code class="language-dockerfile"># Stage 1: Build

FROM golang:1.21-alpine AS builder

WORKDIR /build

COPY . .

RUN go mod download &amp;&amp; \

go build -o /build/app .

Stage 2: Runtime

FROM alpine:3.18

WORKDIR /app

Copiar apenas o binário compilado

COPY --from=builder /build/app .

EXPOSE 8080

CMD [&quot;./app&quot;]</code></pre>

<p>A primeira imagem (<code>builder</code>) contém compilador Go, headers, dependências de desenvolvimento — tudo pesado. A imagem final contém apenas o binário de 10MB. Seu contêiner fica drasticamente menor.</p>

<p>Exemplo com Python:</p>

<pre><code class="language-dockerfile">FROM python:3.11-slim AS builder

WORKDIR /build

COPY requirements.txt .

Compilar wheels em /build/wheels

RUN pip wheel --no-cache-dir --no-deps --wheel-dir /build/wheels -r requirements.txt

Runtime

FROM python:3.11-alpine

WORKDIR /app

Copiar wheels pré-compilados

COPY --from=builder /build/wheels /wheels

Instalar de wheels (rápido, sem compilação)

COPY requirements.txt .

RUN pip install --no-cache /wheels/* &amp;&amp; rm -rf /wheels

COPY app.py .

CMD [&quot;python&quot;, &quot;app.py&quot;]</code></pre>

<p>Multi-stage é a diferença entre uma imagem de 800MB e 200MB.</p>

<h3>.dockerignore: Reduzindo contexto de build</h3>

<p>Quando você executa <code>docker build .</code>, o Docker envia <em>todo</em> o contexto (seu diretório) para o daemon. Arquivos desnecessários aumentam tempo de transferência.</p>

<pre><code># .dockerignore

.git

.gitignore

node_modules

.env

__pycache__

*.pyc

.pytest_cache

.venv

dist

build

*.log

.DS_Store</code></pre>

<p>Isso reduz o contexto drasticamente, acelerando o build.</p>

<h3>Segurança: Princípios básicos</h3>

<p>Nunca rode como root:</p>

<pre><code class="language-dockerfile">FROM python:3.11-alpine

RUN addgroup -S appgroup &amp;&amp; adduser -S appuser -G appgroup

WORKDIR /app

COPY --chown=appuser:appgroup . .

RUN pip install --no-cache-dir -r requirements.txt

USER appuser

CMD [&quot;python&quot;, &quot;app.py&quot;]</code></pre>

<p>Escanear vulnerabilidades:</p>

<pre><code class="language-bash">docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \

aquasec/trivy image myapp:latest</code></pre>

<p>Imagens menores reduzem superfície de ataque. Alpine é mais seguro que Ubuntu por ter menos pacotes.</p>

<h3>Example completo: Aplicação Node.js profissional</h3>

<pre><code class="language-dockerfile"># Stage 1: Builder

FROM node:20-alpine AS builder

WORKDIR /build

COPY package*.json ./

RUN npm ci &amp;&amp; npm run build

Stage 2: Runtime

FROM node:20-alpine

ENV NODE_ENV=production

RUN addgroup -S nodejs &amp;&amp; adduser -S nodejs -G nodejs

WORKDIR /app

COPY --from=builder --chown=nodejs:nodejs /build/dist ./dist

COPY --from=builder --chown=nodejs:nodejs /build/node_modules ./node_modules

COPY --chown=nodejs:nodejs package.json ./

USER nodejs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \

CMD node -e &quot;require(&#039;http&#039;).get(&#039;http://localhost:3000/health&#039;, (r) =&gt; {if (r.statusCode !== 200) throw new Error(r.statusCode)})&quot;

CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code></pre>

<p>Este Dockerfile segue padrões profissionais:</p>

<ul>

<li>Multi-stage para otimização</li>

<li>Não-root user por segurança</li>

<li>HEALTHCHECK para orquestração</li>

<li>NODE_ENV definido</li>

<li>npm ci (não npm install) para builds reproduzíveis</li>

</ul>

<h2>Debugging e Boas Práticas</h2>

<h3>Inspecionando camadas e histórico</h3>

<pre><code class="language-bash"># Ver histórico de camadas

docker history myapp:latest

Inspecionar imagem

docker image inspect myapp:latest | jq &#039;.[]&#039;

Entrar em uma imagem para debugar

docker run -it myapp:latest /bin/sh</code></pre>

<h3>Rebuild sem cache quando necessário</h3>

<pre><code class="language-bash"># Forçar rebuild sem usar cache

docker build --no-cache -t myapp:latest .

Fazer cache apenas até uma instrução específica

docker build --target builder -t myapp:builder .</code></pre>

<h3>Linting e validação</h3>

<p>Use <code>hadolint</code> para validar Dockerfiles:</p>

<pre><code class="language-bash">docker run --rm -i hadolint/hadolint &lt; Dockerfile</code></pre>

<p>Detecta más práticas automaticamente.</p>

<h3>Medindo impacto de mudanças</h3>

<pre><code class="language-bash"># Tamanho total

docker images myapp:latest --format &quot;{{.Size}}&quot;

Tamanho por camada

docker history myapp:latest --human</code></pre>

<h2>Conclusão</h2>

<p>Três aprendizados-chave ao dominar Dockerfiles: <strong>Primeiro</strong>, ordem e agrupamento de instruções impactam drasticamente tempo de build e reuso de cache — coloque instruções que mudam frequentemente por último e combine RUNs com <code>&amp;&amp;</code>. <strong>Segundo</strong>, multi-stage builds são a ferramenta mais poderosa para reduzir tamanho de imagem sem sacrificar funcionalidade — é a diferença entre &quot;funciona&quot; e &quot;profissional&quot;. <strong>Terceiro</strong>, segurança e otimização não são extras — são fundamentais — use usuários não-root, Alpine quando possível, e sempre escanear vulnerabilidades.</p>

<p>Um Dockerfile bem escrito não é mais longo que um mal escrito. A diferença está em cada decisão deliberada: por que essa instrução está nessa ordem? Por que não multi-stage aqui? Essa mentalidade transforma você de alguém que copia exemplos para um profissional que escreve containers eficientes e seguros.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.docker.com/engine/reference/builder/" target="_blank" rel="noopener noreferrer">Docker Official Documentation - Dockerfile Reference</a></li>

<li><a href="https://docs.docker.com/develop/dev-best-practices/dockerfile_best-practices/" target="_blank" rel="noopener noreferrer">Best practices for writing Dockerfiles</a></li>

<li><a href="https://github.com/hadolint/hadolint" target="_blank" rel="noopener noreferrer">Hadolint - Dockerfile Linter</a></li>

<li><a href="https://github.com/aquasecurity/trivy" target="_blank" rel="noopener noreferrer">Aqua Security Trivy - Container Image Scanning</a></li>

<li><a href="https://docs.docker.com/build/building/multi-stage/" target="_blank" rel="noopener noreferrer">Docker Multi-stage Builds Guide</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Docker & Kubernetes

O que Todo Dev Deve Saber sobre Kubernetes Events e Auditoria: Rastreando Mudanças no Cluster
O que Todo Dev Deve Saber sobre Kubernetes Events e Auditoria: Rastreando Mudanças no Cluster

Introdução: Por que Rastrear Mudanças no Kubernetes? No dia a dia de um clust...

Dominando Segurança em Docker: Rootless Containers, Seccomp e AppArmor em Projetos Reais
Dominando Segurança em Docker: Rootless Containers, Seccomp e AppArmor em Projetos Reais

Introdução: O Cenário Atual de Segurança em Containers Docker revolucionou a...

O que Todo Dev Deve Saber sobre Volumes em Docker: bind mounts, named volumes e tmpfs Comparados
O que Todo Dev Deve Saber sobre Volumes em Docker: bind mounts, named volumes e tmpfs Comparados

O Problema: Persistência de Dados em Containers Quando você cria um container...