a11y.skipToContent

Dockerfile e Docker Compose: A arte de construir e orquestrar aplicações otimizadas

Para qualquer desenvolvedor, a complexidade de gerenciar as múltiplas partes de uma aplicação moderna é um desafio constante. Coordenar um banco de dados, um serviço de cache, o backend e o frontend, garantindo que tudo funcione de maneira coesa, pode ser uma tarefa árdua. Manter a consistência entre o ambiente de desenvolvimento local, os testes e a produção apenas adiciona mais uma camada de dificuldade a esse cenário.

É aqui que o Dockerfile e o Docker Compose entram como ferramentas essenciais para resolver esse problema. Com eles, é possível criar “receitas” padronizadas para cada parte da sua aplicação e orquestrar todo o conjunto com uma simplicidade impressionante. Este artigo guiará você por todo o processo, desde a teoria fundamental da construção de imagens até as técnicas avançadas de otimização e orquestração.

Desvendando o Dockerfile

O que é um Dockerfile?

Um Dockerfile é um arquivo de texto que funciona como uma “receita de bolo” para construir uma imagem Docker. Pense na imagem como um pacote completo e imutável que contém tudo o que sua aplicação precisa para rodar: o código, as dependências, as bibliotecas e as configurações. O contêiner, por sua vez, é a instância em execução dessa imagem. O fluxo é simples e direto: você escreve um&nbsp;<code>Dockerfile</code>, usa o comando&nbsp;<code>docker build</code>&nbsp;para criar uma Imagem a partir dele e, finalmente, executa essa imagem como um Contêiner com o comando&nbsp;<code>docker run</code>.

Comandos fundamentais

Um Dockerfile é composto por uma série de instruções. As mais comuns e essenciais são:

  • FROM: É o ponto de partida obrigatório. Define a imagem base sobre a qual sua própria imagem será construída. Por exemplo,&nbsp;FROM node:18-alpine.
  • WORKDIR: Define o diretório de trabalho para todos os comandos que vêm a seguir (RUN,&nbsp;CMD,&nbsp;COPY, etc.). É como executar um&nbsp;cd&nbsp;antes de começar a trabalhar dentro da imagem.
  • COPY: Copia arquivos e diretórios da sua máquina local (o host) para dentro da imagem. É usado para adicionar o código-fonte e os arquivos de configuração da sua aplicação.
  • RUN: Executa comandos durante o processo de construção da imagem. É ideal para tarefas como a instalação de dependências (ex:&nbsp;RUN npm install). Cada instrução&nbsp;RUN&nbsp;cria uma nova camada na imagem, o que tem implicações de performance e cache que detalharemos na seção de otimização.
  • ENV: Define variáveis de ambiente. Elas ficam disponíveis tanto durante o processo de build quanto para os contêineres em execução. Exemplo:&nbsp;ENV NODE_ENV=production.
  • EXPOSE: Serve como documentação, indicando em qual porta a aplicação dentro do contêiner estará escutando. É uma forma de comunicar ao usuário que for executar o contêiner qual porta ele provavelmente desejará mapear. Esta instrução não publica a porta de fato.

CMD vs. ENTRYPOINT

Uma fonte comum de confusão é a diferença entre&nbsp;<code>CMD</code>&nbsp;e&nbsp;<code>ENTRYPOINT</code>. Ambas definem o que será executado quando o contêiner iniciar, mas com propósitos distintos:

  • ENTRYPOINT: Define o executável principal do contêiner. A intenção é que a imagem se comporte como aquele executável. O comando definido aqui não é facilmente sobrescrito.
  • CMD: Fornece os argumentos padrão para o&nbsp;ENTRYPOINT. Estes argumentos&nbsp;são&nbsp;facilmente sobrescritos ao iniciar o contêiner com&nbsp;docker run.

Quando usados juntos, eles se tornam muito poderosos. Imagine criar uma imagem que se comporte como o comando&nbsp;<code>git</code>:

ENTRYPOINT ["/usr/bin/git"]
CMD ["--help"]

Com essa configuração:

  • docker run minha-imagem-git&nbsp;executaria&nbsp;git --help, pois&nbsp;--help&nbsp;é o&nbsp;CMD&nbsp;padrão.
  • docker run minha-imagem-git status&nbsp;executaria&nbsp;git status, pois&nbsp;status&nbsp;sobrescreve o&nbsp;CMD&nbsp;padrão, sendo passado como argumento para o&nbsp;ENTRYPOINT.

Se apenas um&nbsp;<code>CMD</code>&nbsp;é usado, ele define tanto o executável quanto os argumentos, e ambos podem ser sobrescritos.

Otimizando suas imagens Docker

Criar uma imagem funcional é apenas o primeiro passo. Construir uma imagem otimizada — pequena, rápida e segura — é o que diferencia uma configuração amadora de uma profissional.

A escolha da imagem base

A otimização começa com a instrução&nbsp;<code>FROM</code>. Use imagens base mínimas de fontes confiáveis, como as&nbsp;Docker Official Images&nbsp;ou&nbsp;Verified Publisher Images. Imagens como&nbsp;<code>alpine</code>&nbsp;são extremamente leves, enquanto as&nbsp;<code>distroless</code>&nbsp;vão um passo além, contendo apenas a aplicação e suas dependências, sem shells ou gerenciadores de pacotes.

O impacto disso é direto: imagens menores resultam em execuções mais rápidas nos pipelines de CI/CD, escalonamento mais ágil em orquestradores como Kubernetes e, crucialmente, uma superfície de ataque significativamente menor, o que torna seu contêiner mais seguro.

A mágica dos multi-stage builds

Esta é uma das técnicas de otimização mais impactantes. A lógica é usar múltiplos estágios&nbsp;<code>FROM</code>&nbsp;em um único Dockerfile. O primeiro estágio, o "build stage" (ex:&nbsp;<code>FROM node:18 as build</code>), é usado para compilar o código e instalar todas as dependências de desenvolvimento. Um segundo estágio, muito mais enxuto (ex:&nbsp;<code>FROM nginx:alpine</code>), apenas copia os artefatos finais do primeiro.

Por exemplo, para uma aplicação frontend:

# Estágio 1: Build da aplicação
FROM node:18 as build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# Estágio 2: Imagem final de produção
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

O resultado é uma imagem final contendo apenas o servidor Nginx e os arquivos estáticos compilados, eliminando o Node.js,&nbsp;<code>node_modules</code>&nbsp;e todas as ferramentas de build. A redução de tamanho pode ser superior a 80%.

Camadas e Cache: Entendendo o processo de build

Cada instrução em um Dockerfile cria uma camada. O Docker utiliza o cache dessas camadas para acelerar builds subsequentes. Duas dicas práticas surgem disso:

  1. Agrupe comandos e limpe o lixo:&nbsp;Combine múltiplos comandos&nbsp;RUN&nbsp;em uma única instrução usando&nbsp;&amp;&amp;. Mais importante ainda, limpe os artefatos de instalação na mesma camada para reduzir o tamanho da imagem. O bom exemplo não só reduz o número de camadas, mas também adiciona&nbsp;--no-install-recommends&nbsp;para evitar pacotes desnecessários e remove o cache do&nbsp;apt&nbsp;na mesma instrução, garantindo que ele não ocupe espaço na imagem final.
  2. Ordene as instruções para maximizar o cache:&nbsp;Coloque as instruções que mudam com menos frequência no início do arquivo. A alteração de uma camada invalida o cache de todas as camadas subsequentes (um efeito conhecido como “cache busting”). O padrão mais comum é instalar dependências&nbsp;antes&nbsp;de copiar o código-fonte. Como&nbsp;package.json&nbsp;muda com menos frequência que o código-fonte, o Docker pode reutilizar a camada do&nbsp;npm install&nbsp;na maioria dos builds, economizando um tempo considerável.

Não leve lixo: O poder do&nbsp;.dockerignore

Assim como o&nbsp;<code>.gitignore</code>&nbsp;impede que arquivos sejam enviados para seu repositório Git, o arquivo&nbsp;<code>.dockerignore</code>&nbsp;impede que arquivos e pastas desnecessários (como&nbsp;<code>.git</code>,&nbsp;<code>node_modules</code>&nbsp;ou logs locais) sejam enviados para o daemon do Docker. Isso acelera o contexto do build e evita que segredos ou arquivos indesejados acabem acidentalmente na sua imagem final.

O papel do Docker Compose

O que é e por que usar o Docker Compose?

Enquanto o Dockerfile define a receita para um único contêiner, o Docker Compose é a ferramenta para definir e gerenciar uma aplicação completa composta por múltiplos contêineres. Imagine uma aplicação web típica: um backend, um banco de dados e um serviço de cache. Com o Docker Compose, você descreve todos esses serviços, suas redes e volumes em um único arquivo, e pode subir todo o ambiente com um simples comando:&nbsp;<code>docker compose up</code>.

Anatomia de um arquivo

O coração do Docker Compose é o arquivo&nbsp;<code>docker-compose.yml</code>. Veja um exemplo mais realista que define dois serviços (<code>web</code>&nbsp;e&nbsp;<code>redis</code>), incluindo configuração e persistência de dados.

services:
  web:
    build: .
    ports:
      - "8000:5000"
    environment:
      - REDIS_HOST=redis
    volumes:
      - .:/code
redis:
    image: "redis:alpine"
    volumes:
      - redis-data:/data
volumes:
  redis-data:

Vamos entender as partes principais:

  • services: Onde cada contêiner (serviço) da sua aplicação é definido.
  • web&nbsp;e&nbsp;redis: São os nomes dos seus serviços. Um benefício poderoso é que esses nomes também funcionam como&nbsp;hostnames&nbsp;na rede interna que o Docker Compose cria automaticamente para o projeto. Isso significa que o serviço&nbsp;web&nbsp;pode se conectar ao Redis usando o endereço&nbsp;redis:6379.
  • build: .: Instrução para construir a imagem para o serviço&nbsp;web&nbsp;usando o&nbsp;Dockerfile&nbsp;no diretório atual.
  • image: "redis:alpine": Instrução para usar uma imagem pública do Docker Hub, sem a necessidade de um build local.
  • ports: - "8000:5000": Mapeia a porta&nbsp;8000&nbsp;da sua máquina (host) para a porta&nbsp;5000&nbsp;dentro do contêiner&nbsp;web, permitindo o acesso externo.
  • environment: Permite passar variáveis de ambiente para o contêiner, uma forma comum de configurar aplicações.
  • volumes: Define como os dados são gerenciados. No serviço&nbsp;web,&nbsp;.:/code&nbsp;é um&nbsp;bind mount, que espelha o diretório atual na máquina host para dentro do contêiner, ideal para desenvolvimento. No serviço&nbsp;redis,&nbsp;redis-data:/data&nbsp;usa um&nbsp;named volume&nbsp;(redis-data), que é gerenciado pelo Docker para persistir os dados do banco de dados mesmo que o contêiner seja removido. O volume nomeado é declarado no bloco&nbsp;volumes&nbsp;no final do arquivo.

Conclusão

Neste artigo, percorremos uma jornada completa: começamos criando a “receita” de um contêiner com o Dockerfile, aprendemos técnicas cruciais para torná-la eficiente e enxuta, e finalizamos orquestrando uma aplicação complexa com o Docker Compose. Dominar essas ferramentas é um passo fundamental para criar ambientes de desenvolvimento e produção que sejam robustos, padronizados e reprodutíveis — um dos pilares centrais da cultura DevOps.

Agora que você sabe como construir imagens otimizadas e orquestrá-las com facilidade, qual aplicação complexa você vai containerizar a seguir?