<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 "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.</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 && \
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 \
&& 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 && \
tar -xzf /tmp/archive.tar.gz -C /tmp && \
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 ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]</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 "Build date: $BUILD_DATE" && echo "Version: $VERSION"</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 ["python", "app.py"]
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 ["python", "app.py"]
CMD ["--port", "8000"]
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 "exec" (array JSON) é preferível à forma shell:</p>
<pre><code class="language-dockerfile"># Ruim - exec em shell, sinais não funcionam corretamente
ENTRYPOINT "python app.py"
Bom - exec direto
ENTRYPOINT ["python", "app.py"]</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 "builder" 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 && \
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 ["./app"]</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/* && rm -rf /wheels
COPY app.py .
CMD ["python", "app.py"]</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 && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
CMD ["python", "app.py"]</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 && npm run build
Stage 2: Runtime
FROM node:20-alpine
ENV NODE_ENV=production
RUN addgroup -S nodejs && 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 "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
CMD ["node", "dist/server.js"]</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 '.[]'
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 < 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 "{{.Size}}"
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>&&</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 "funciona" e "profissional". <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><!-- FIM --></p>