Docker & Kubernetes

Boas Práticas de Imagens Docker: Layers, Union Filesystem e Como o Build Realmente Funciona para Times Ágeis

14 min de leitura

Boas Práticas de Imagens Docker: Layers, Union Filesystem e Como o Build Realmente Funciona para Times Ágeis

Introdução: O Que São Layers e Por Que Importam Docker revolucionou a forma como empacotamos e entregamos aplicações. Mas por trás da simplicidade de um comando existe uma arquitetura sofisticada baseada em layers e union filesystem. 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. Uma imagem Docker não é um arquivo monolítico. Ela é, na verdade, uma pilha de camadas imutáveis (layers), 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. Compreendendo Layers: A Arquitetura em Camadas O Que é uma Layer? Uma layer é um arquivo contendo as mudanças (delta) em relação à camada anterior. Quando você escreve um

<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 &amp;&amp; apt-get install -y python3

RUN pip3 install flask

COPY app.py /app/

WORKDIR /app

CMD [&quot;python3&quot;, &quot;app.py&quot;]</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 [&quot;/bin/bash&quot;] 0B

a1b2c3d4e5f6 2 weeks ago /bin/sh -c mkdir -p /run/systemd &amp;&amp; echo &#039;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 &quot;RootFS&quot;</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 &lt;container_id&gt; | grep -A 10 &quot;GraphDriver&quot;</code></pre>

<p>Saída tipicamente mostrará algo como:</p>

<pre><code class="language-json">&quot;GraphDriver&quot;: {

&quot;Data&quot;: {

&quot;LowerDir&quot;: &quot;/var/lib/docker/overlay2/layer1:/var/lib/docker/overlay2/layer2:/var/lib/docker/overlay2/layer3&quot;,

&quot;MergedDir&quot;: &quot;/var/lib/docker/overlay2/merged&quot;,

&quot;UpperDir&quot;: &quot;/var/lib/docker/overlay2/layer4/diff&quot;,

&quot;WorkDir&quot;: &quot;/var/lib/docker/overlay2/layer4/work&quot;

},

&quot;Name&quot;: &quot;overlay2&quot;

}</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=&quot;seu-email@example.com&quot;

ENV PYTHONUNBUFFERED=1

Layer 2: Dependências do sistema

RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \

build-essential \

&amp;&amp; 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 [&quot;python&quot;, &quot;app.py&quot;]</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

---&gt; a1b2c3d4e5f6

Step 2/8 : LABEL maintainer=&quot;seu-email@example.com&quot;

---&gt; Running in temporary_container_abc123

---&gt; a2b3c4d5e6f7

Step 3/8 : ENV PYTHONUNBUFFERED=1

---&gt; Running in temporary_container_def456

---&gt; b3c4d5e6f7g8

Step 4/8 : RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends...

---&gt; Running in temporary_container_ghi789

---&gt; c4d5e6f7g8h9

Step 5/8 : WORKDIR /app

---&gt; Running in temporary_container_jkl012

---&gt; d5e6f7g8h9i0

Step 6/8 : COPY requirements.txt .

---&gt; e6f7g8h9i0j1

Step 7/8 : RUN pip install --no-cache-dir -r requirements.txt

---&gt; Running in temporary_container_mno345

---&gt; f7g8h9i0j1k2

Step 8/8 : COPY . .

---&gt; 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 [&quot;node&quot;, &quot;index.js&quot;]</code></pre>

<p>Este Dockerfile tem vários problemas:</p>

<ol>

<li>Usa a imagem completa do Node (&gt;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 [&quot;node&quot;, &quot;index.js&quot;]</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Docker & Kubernetes

Boas Práticas de ArgoCD: GitOps Contínuo, App of Apps e Sync Policies para Times Ágeis
Boas Práticas de ArgoCD: GitOps Contínuo, App of Apps e Sync Policies para Times Ágeis

GitOps e ArgoCD: Fundamentos GitOps é um paradigma operacional onde o Git se...

Dominando Pods em Kubernetes: Ciclo de Vida, Init Containers e Sidecar Pattern em Projetos Reais
Dominando Pods em Kubernetes: Ciclo de Vida, Init Containers e Sidecar Pattern em Projetos Reais

Entendendo Pods: A Unidade Fundamental do Kubernetes Um Pod é a menor unidade...

O que Todo Dev Deve Saber sobre Istio em Kubernetes: Instalação, Traffic Management e mTLS
O que Todo Dev Deve Saber sobre Istio em Kubernetes: Instalação, Traffic Management e mTLS

Introdução ao Istio: O Service Mesh que Seu Kubernetes Precisa O Istio é um s...