<h2>Introdução: O Que São Layers e Por Que Importam</h2>
<p>Docker revolucionou a forma como empacotamos e entregamos aplicações. Mas por trás da simplicidade de um comando <code>docker run</code> existe uma arquitetura sofisticada baseada em <strong>layers</strong> e <strong>union filesystem</strong>. Compreender esses conceitos é essencial para escrever Dockerfiles eficientes, otimizar tamanho de imagens e debugar problemas de construção. Neste artigo, vamos desconstruir como Docker realmente funciona por trás dos bastidores.</p>
<p>Uma imagem Docker não é um arquivo monolítico. Ela é, na verdade, uma <strong>pilha de camadas imutáveis (layers)</strong>, cada uma representando um conjunto de mudanças no sistema de arquivos. Quando você cria um container a partir de uma imagem, Docker adiciona uma camada gravável no topo, permitindo que o container faça alterações sem modificar a imagem original. Esse é o segredo da eficiência do Docker.</p>
<h2>Compreendendo Layers: A Arquitetura em Camadas</h2>
<h3>O Que é uma Layer?</h3>
<p>Uma layer é um arquivo contendo as mudanças (delta) em relação à camada anterior. Quando você escreve um Dockerfile com múltiplos comandos <code>RUN</code>, <code>COPY</code>, <code>ADD</code> ou <code>ENV</code>, cada um desses comandos gera uma nova layer. Essa abordagem permite reutilização: se duas imagens compartilham as mesmas camadas iniciais, elas reutilizam o mesmo espaço em disco.</p>
<p>Vamos visualizar isso com um exemplo prático. Imagine um Dockerfile simples:</p>
<pre><code class="language-dockerfile">FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
RUN pip3 install flask
COPY app.py /app/
WORKDIR /app
CMD ["python3", "app.py"]</code></pre>
<p>Cada linha que modifica o sistema de arquivos cria uma layer separada. A imagem final é a composição dessas camadas. Se você mudar apenas a linha <code>COPY app.py /app/</code>, as três primeiras layers podem ser reutilizadas do cache de build anterior, acelerando significativamente a construção.</p>
<h3>Inspecionando Layers com Docker History</h3>
<p>Você pode examinar as layers de qualquer imagem usando <code>docker history</code>. Este comando mostra o histórico de construção e o tamanho de cada layer:</p>
<pre><code class="language-bash">docker history ubuntu:22.04</code></pre>
<p>Saída esperada:</p>
<pre><code>IMAGE CREATED CREATED BY SIZE
baasf8d92b3 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
a1b2c3d4e5f6 2 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 16.8MB
g7h8i9j0k1l2 2 weeks ago /bin/sh -c #(nop) ADD file:1234... 77.8MB</code></pre>
<p>Cada linha representa uma layer. O <code>SIZE</code> mostra quanto essa camada contribui para o tamanho total. Camadas com size 0B geralmente são metadados (como <code>CMD</code> ou <code>ENV</code>).</p>
<p>Para uma inspeção mais detalhada, você pode usar <code>docker inspect</code> com a flag de imagem:</p>
<pre><code class="language-bash">docker inspect ubuntu:22.04 | grep -A 20 "RootFS"</code></pre>
<p>Isso mostra os digests criptográficos (SHA256) de cada layer, permitindo verificar exatamente quais camadas uma imagem contém.</p>
<h2>Union Filesystem: Como Docker Monta as Camadas</h2>
<h3>O Mecanismo Por Trás da Montagem</h3>
<p>Docker usa um <strong>union filesystem</strong> para sobrepor camadas de forma eficiente. Um union filesystem permite que múltiplos diretórios sejam montados no mesmo ponto, criando uma visão unificada do sistema de arquivos. O driver mais comum atualmente é o <strong>overlay2</strong>, que substituiu o antigo AUFS.</p>
<p>Quando um container inicia, Docker monta as layers em ordem:</p>
<ol>
<li>A layer base (geralmente uma distribuição Linux como Ubuntu ou Alpine)</li>
<li>Camadas intermediárias (instalação de pacotes, adição de arquivos)</li>
<li>Uma camada gravável no topo (exclusive para esse container)</li>
</ol>
<p>Qualquer leitura de arquivo passa pelas camadas de cima para baixo até encontrar o arquivo. Escritas sempre vão para a camada gravável do topo. Se você modifica um arquivo que existe em uma camada inferior, Docker copia o arquivo para a camada do container (copy-on-write) antes de modificá-lo. Isso preserva a integridade da imagem original.</p>
<h3>Visualizando o Union Filesystem</h3>
<p>Você pode inspecionar a estrutura de layers de um container usando:</p>
<pre><code class="language-bash">docker inspect <container_id> | grep -A 10 "GraphDriver"</code></pre>
<p>Saída tipicamente mostrará algo como:</p>
<pre><code class="language-json">"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/layer1:/var/lib/docker/overlay2/layer2:/var/lib/docker/overlay2/layer3",
"MergedDir": "/var/lib/docker/overlay2/merged",
"UpperDir": "/var/lib/docker/overlay2/layer4/diff",
"WorkDir": "/var/lib/docker/overlay2/layer4/work"
},
"Name": "overlay2"
}</code></pre>
<ul>
<li><strong>LowerDir</strong>: As layers de somente leitura (imagem)</li>
<li><strong>UpperDir</strong>: A layer gravável (específica do container)</li>
<li><strong>MergedDir</strong>: A visão unificada (onde o container enxerga o filesystem)</li>
<li><strong>WorkDir</strong>: Diretório temporário para operações do filesystem</li>
</ul>
<h2>Como o Docker Build Realmente Funciona</h2>
<h3>O Processo Passo a Passo</h3>
<p>Quando você executa <code>docker build</code>, Docker passa por um processo determinístico:</p>
<ol>
<li><strong>Parsing do Dockerfile</strong>: Docker lê e valida a sintaxe</li>
<li><strong>Identificação de Cache</strong>: Para cada instrução, Docker verifica se existe uma layer anterior com aquela combinação de comando e seus contextos</li>
<li><strong>Execução</strong>: Se houver cache, reutiliza; caso contrário, cria um container temporário, executa a instrução e gera uma nova layer</li>
<li><strong>Remoção do Container Temporário</strong>: O container intermediário é descartado, mas sua layer é preservada</li>
</ol>
<p>Vamos criar um exemplo realista de um Dockerfile para uma aplicação Python:</p>
<pre><code class="language-dockerfile">FROM python:3.11-slim
Layer 1: Metadados
LABEL maintainer="seu-email@example.com"
ENV PYTHONUNBUFFERED=1
Layer 2: Dependências do sistema
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
Layer 3: Código-fonte
WORKDIR /app
COPY requirements.txt .
Layer 4: Dependências Python
RUN pip install --no-cache-dir -r requirements.txt
Layer 5: Aplicação
COPY . .
Metadados (não cria layer)
EXPOSE 5000
CMD ["python", "app.py"]</code></pre>
<p>Vamos construir essa imagem e entender o que acontece:</p>
<pre><code class="language-bash">docker build -t minha-app:1.0 .</code></pre>
<p>Saída esperada:</p>
<pre><code>Sending build context to Docker daemon 15.36kB
Step 1/8 : FROM python:3.11-slim
---> a1b2c3d4e5f6
Step 2/8 : LABEL maintainer="seu-email@example.com"
---> Running in temporary_container_abc123
---> a2b3c4d5e6f7
Step 3/8 : ENV PYTHONUNBUFFERED=1
---> Running in temporary_container_def456
---> b3c4d5e6f7g8
Step 4/8 : RUN apt-get update && apt-get install -y --no-install-recommends...
---> Running in temporary_container_ghi789
---> c4d5e6f7g8h9
Step 5/8 : WORKDIR /app
---> Running in temporary_container_jkl012
---> d5e6f7g8h9i0
Step 6/8 : COPY requirements.txt .
---> e6f7g8h9i0j1
Step 7/8 : RUN pip install --no-cache-dir -r requirements.txt
---> Running in temporary_container_mno345
---> f7g8h9i0j1k2
Step 8/8 : COPY . .
---> g8h9i0j1k2l3
Successfully built g8h9i0j1k2l3</code></pre>
<h3>Otimizando o Build com Cache</h3>
<p>O cache é um tópico crítico. Docker usa uma <strong>estratégia de hash</strong> para determinar se pode reutilizar uma layer. Se você mudar o <code>requirements.txt</code> mas não a instrução <code>RUN apt-get update</code>, Docker pode reutilizar o resultado anterior dessa instrução se o comando e o contexto de build forem idênticos.</p>
<p>Porém, há um detalhe importante: <strong>a ordem importa</strong>. Se você coloca <code>COPY . .</code> antes de instalar dependências, qualquer mudança no código fonte invalida o cache de tudo que vem depois. Por isso, a ordem recomendada é:</p>
<pre><code class="language-dockerfile"></code></pre>
<p>Não faça assim:</p>
<pre><code class="language-dockerfile"># ❌ RUIM: Código antes de dependências invalida cache facilmente
FROM python:3.11-slim
COPY . .
RUN pip install -r requirements.txt</code></pre>
<p>Você também pode desabilitar completamente o cache usando a flag <code>--no-cache</code>:</p>
<pre><code class="language-bash">docker build --no-cache -t minha-app:1.0 .</code></pre>
<h3>Inspecionando Layers Específicas</h3>
<p>Para entender exatamente o que mudou em cada layer, você pode criar um container a partir de uma imagem intermediária:</p>
<pre><code class="language-bash"># Usando o hash intermediário do build
docker run -it c4d5e6f7g8h9 bash</code></pre>
<p>Isso abre um shell dentro daquela layer específica, permitindo inspecionar exatamente qual foi o resultado da execução.</p>
<h2>Exemplo Prático: Otimizando Uma Imagem Real</h2>
<p>Vamos demonstrar um caso de uso real: reduzir o tamanho de uma imagem de aplicação Node.js. Primeiro, um Dockerfile ingênuo:</p>
<pre><code class="language-dockerfile">FROM node:18
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "index.js"]</code></pre>
<p>Este Dockerfile tem vários problemas:</p>
<ol>
<li>Usa a imagem completa do Node (>900MB)</li>
<li>Instala dependências desnecessárias de desenvolvimento</li>
<li>Não aproveita adequadamente o cache</li>
</ol>
<p>Aqui está a versão otimizada:</p>
<pre><code class="language-dockerfile"># Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
Copiar apenas as dependências instaladas do stage anterior
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]</code></pre>
<p>As melhorias aqui são:</p>
<ul>
<li><strong>Alpine</strong>: Reduz de 900MB para ~170MB</li>
<li><strong>Multi-stage build</strong>: Apenas a imagem final inclui <code>node_modules</code> relevantes</li>
<li><strong>npm ci</strong>: Mais determinístico que <code>npm install</code></li>
<li><strong>Separação clara</strong>: O builder tem tudo; o runtime apenas o necessário</li>
</ul>
<p>Verifique o tamanho:</p>
<pre><code class="language-bash"># Versão ingênua
docker build -t app-ingénua:1.0 .
docker images app-ingénua
Versão otimizada
docker build -t app-otimizada:1.0 .
docker images app-otimizada</code></pre>
<p>Esperamos uma redução de <strong>50-70%</strong> no tamanho da imagem.</p>
<h2>Conclusão</h2>
<p>Aprendemos que imagens Docker são construídas como <strong>pilhas de camadas imutáveis</strong>, onde cada instrução no Dockerfile gera uma layer separada. O <strong>union filesystem</strong> (overlay2) permite montar essas camadas de forma eficiente, criando uma visão unificada do filesystem enquanto preserva a integridade da imagem original através do mecanismo copy-on-write.</p>
<p>Compreender o <strong>processo de build e cache</strong> é fundamental: Docker compara hashes de instruções e contextos para reutilizar layers, economizando tempo e espaço. A ordem das instruções no Dockerfile é crítica — colocar mudanças frequentes por último maximiza o cache. Finalmente, <strong>técnicas como multi-stage builds</strong> e uso de imagens Alpine reduzem drasticamente o tamanho da imagem sem sacrificar funcionalidade.</p>
<p>Com esse conhecimento, você está equipado para construir imagens Docker eficientes, entender por que builds falham, debugar problemas de cache e otimizar o tempo de deployment de suas aplicações.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.docker.com/storage/storagedriver/" target="_blank" rel="noopener noreferrer">Docker Documentation - Layers</a></li>
<li><a href="https://docs.docker.com/storage/storagedriver/overlayfs-driver/" target="_blank" rel="noopener noreferrer">Docker Documentation - Union File System</a></li>
<li><a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/" target="_blank" rel="noopener noreferrer">Docker Best Practices - Dockerfile</a></li>
<li><a href="https://www.dockerbook.com/" target="_blank" rel="noopener noreferrer">The Docker Book - James Turnbull</a></li>
<li><a href="https://docs.docker.com/build/cache/" target="_blank" rel="noopener noreferrer">Understanding Docker Image Layers and Cache</a></li>
</ul>
<p><!-- FIM --></p>