O que Todo Dev Deve Saber sobre Docker Compose para Desenvolvimento: Hot Reload e Ambientes Reproduzíveis Já leu

Docker Compose para Desenvolvimento: Hot Reload e Ambientes Reproduzíveis O Docker Compose é uma ferramenta fundamental para qualquer desenvolvedor que trabalhe com microserviços ou aplicações containerizadas. Ele permite definir e executar múltiplos containers de forma orquestrada através de um único arquivo YAML. Neste artigo, vamos além da configuração básica e mergulharemos em estratégias práticas para implementar hot reload durante o desenvolvimento e criar ambientes que funcionam identicamente em qualquer máquina. A principal vantagem de usar Docker Compose em desenvolvimento é eliminar o famoso "funciona na minha máquina" — você garante que todos os desenvolvedores trabalhem com as mesmas versões de dependências, banco de dados e serviços. Além disso, o hot reload permite que você veja mudanças em tempo real sem reiniciar containers, acelerando significativamente o ciclo de desenvolvimento. Fundamentos do Docker Compose e Volumes O que é Docker Compose e por que importa Docker Compose funciona através de um arquivo que descreve todos os serviços da sua aplicação em um

Docker Compose para Desenvolvimento: Hot Reload e Ambientes Reproduzíveis

O Docker Compose é uma ferramenta fundamental para qualquer desenvolvedor que trabalhe com microserviços ou aplicações containerizadas. Ele permite definir e executar múltiplos containers de forma orquestrada através de um único arquivo YAML. Neste artigo, vamos além da configuração básica e mergulharemos em estratégias práticas para implementar hot reload durante o desenvolvimento e criar ambientes que funcionam identicamente em qualquer máquina.

A principal vantagem de usar Docker Compose em desenvolvimento é eliminar o famoso "funciona na minha máquina" — você garante que todos os desenvolvedores trabalhem com as mesmas versões de dependências, banco de dados e serviços. Além disso, o hot reload permite que você veja mudanças em tempo real sem reiniciar containers, acelerando significativamente o ciclo de desenvolvimento.

Fundamentos do Docker Compose e Volumes

O que é Docker Compose e por que importa

Docker Compose funciona através de um arquivo docker-compose.yml que descreve todos os serviços da sua aplicação em um formato declarativo. Cada serviço é essencialmente um container, mas em vez de gerenciar comandos docker run complexos, você define tudo uma vez e executa docker-compose up. A grande diferença em relação ao Docker tradicional é que o Compose gerencia o networking entre containers automaticamente, permitindo que eles se comuniquem apenas pelo nome do serviço.

Para desenvolvimento específico, o conceito crucial é o de volumes. Volumes permitem compartilhar diretórios entre seu sistema de arquivos local e o container. Diferentemente das imagens Docker, que são imutáveis, volumes persistem alterações. Isso é fundamental para hot reload — quando você edita um arquivo no seu editor local, a mudança aparece instantaneamente dentro do container.

Configurando volumes para sincronização em tempo real

Existem três tipos de volumes em Docker: volumes nomeados (gerenciados pelo Docker), bind mounts (apontam para caminhos específicos) e tmpfs (em memória). Para desenvolvimento, usaremos bind mounts porque queremos que os arquivos do projeto local sejam refletidos dentro do container.

version: '3.9'

services:
  app:
    build: .
    container_name: minha_app
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development

Neste exemplo, .:/app mapeia o diretório atual do seu computador para /app dentro do container. A segunda linha /app/node_modules é importante: ela cria um volume anônimo para node_modules, impedindo que o diretório local sobrescreva as dependências instaladas no container. Isso é fundamental porque as dependências do seu sistema operacional podem ser incompatíveis com as do container.

Implementando Hot Reload em Diferentes Linguagens

Node.js com Nodemon

Para aplicações Node.js, o Nodemon é a escolha padrão. Ele monitora mudanças nos arquivos e reinicia automaticamente o processo Node.js. Configurar isso no Docker é simples, mas requer atenção aos detalhes.

Primeiro, instale o nodemon como dependência de desenvolvimento:

npm install --save-dev nodemon

Crie um arquivo nodemon.json:

{
  "watch": ["src"],
  "ext": "js,json",
  "ignore": ["node_modules"],
  "exec": "node",
  "delay": 500
}

Agora configure o docker-compose.yml:

version: '3.9'

services:
  app:
    build: .
    container_name: node_app_dev
    volumes:
      - .:/app
      - /app/node_modules
    working_dir: /app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
    command: nodemon src/index.js
    stdin_open: true
    tty: true

O Dockerfile correspondente seria:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "src/index.js"]

Quando você executa docker-compose up, o Nodemon dentro do container monitora o diretório /app (que está sincronizado com seu código local). Sempre que um arquivo é alterado, o Node.js é automaticamente reiniciado. Os parâmetros stdin_open: true e tty: true garantem que você possa ver o output e até mesmo enviar sinais de interrupção (Ctrl+C) para o container.

Python com Watchdog

Para Python, a abordagem é similar, mas usaremos Watchdog ou simplesmente recarregamento de módulo com Flask/Django. Para uma API Flask:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

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

COPY . .

EXPOSE 5000

CMD ["flask", "run", "--host=0.0.0.0"]
version: '3.9'

services:
  app:
    build: .
    container_name: python_app_dev
    volumes:
      - .:/app
    ports:
      - "5000:5000"
    environment:
      - FLASK_APP=main.py
      - FLASK_ENV=development
    command: flask run --host=0.0.0.0

Flask em modo desenvolvimento já recarrega automaticamente quando detecta mudanças. O parâmetro FLASK_ENV=development ativa esse comportamento. Para mais controle, você pode usar:

RUN pip install --no-cache-dir -r requirements.txt watchdog[watchmedo]

E alterar o comando para:

command: watchmedo auto-restart -d /app -p '*.py' -- python main.py

Ambientes Reproduzíveis: Beyond the Basics

Arquitetura Multi-Container com Banco de Dados

O verdadeiro poder do Docker Compose em desenvolvimento aparece quando você precisa orchestrar múltiplos serviços. Considere uma aplicação Node.js com PostgreSQL e Redis:

version: '3.9'

services:
  api:
    build: .
    container_name: api_dev
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    command: nodemon src/index.js

  db:
    image: postgres:15-alpine
    container_name: postgres_dev
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: redis_dev
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Nesta configuração, a comunicação entre serviços acontece pelo nome: db e cache são nomes de host válidos dentro da rede Docker criada pelo Compose. A variável DATABASE_URL aponta para db:5432 em vez de localhost:5432 porque estamos dentro da rede Docker, não do seu sistema operacional.

O arquivo init.sql é particularmente útil — ele é executado automaticamente quando o container PostgreSQL inicia pela primeira vez, criando tabelas e inserindo dados de teste. Isso garante que cada desenvolvedor tenha a mesma estrutura de banco de dados.

Versionamento de Dependências e Reprodutibilidade

Uma causa comum de inconsistência entre ambientes é variações de versão. O Docker resolve isso através de imagens, mas você deve ser explícito:

services:
  db:
    image: postgres:15.2-alpine  # Versão específica, não 15-alpine
    # ...

  cache:
    image: redis:7.0.11-alpine   # Sempre especifique versão exata
    # ...

Para sua aplicação, use lock files. Em Node.js, isso é package-lock.json. Em Python, requirements.txt com versões fixas ou poetry.lock:

# requirements.txt
Flask==2.3.2
SQLAlchemy==2.0.19
psycopg2-binary==2.9.7
redis==5.0.0
# docker-compose.yml
api:
  build:
    context: .
    dockerfile: Dockerfile
    # Você pode adicionar args se necessário

Commit esses arquivos no Git. Assim, quando um novo desenvolvedor clona o repositório e executa docker-compose up, ele obtém exatamente as mesmas versões que você estava usando.

Configuração Avançada e Otimizações

Usando .env para Variáveis de Ambiente

Embora seja tentador hardcoding valores no docker-compose.yml, o certo é usar variáveis de ambiente. Crie um arquivo .env na raiz do projeto:

DATABASE_URL=postgresql://postgres:devpassword@db:5432/myapp
REDIS_URL=redis://cache:6379
API_PORT=3000
DEBUG=true

Docker Compose carrega automaticamente esse arquivo. No seu docker-compose.yml:

services:
  api:
    ports:
      - "${API_PORT}:3000"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - DEBUG=${DEBUG}

Para produção, use um .env.production diferente (não commit no Git) ou variáveis de ambiente do seu orquestrador (Kubernetes, AWS ECS, etc.).

Estratégias de Networking e Comunicação

Docker Compose cria uma rede padrão onde todos os serviços podem se comunicar. Para casos mais complexos, você pode criar redes explícitas:

version: '3.9'

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

services:
  api:
    networks:
      - backend
    # ...

  db:
    networks:
      - backend
    # ...

  web:
    networks:
      - frontend
    # ...

Isso é útil quando você quer isolar certos serviços. Por exemplo, um container de teste nunca precisa acessar o banco de dados diretamente — apenas a API faz isso.

Health Checks para Startup Order

Um problema comum é que o depends_on apenas garante que o container inicie, não que ele esteja pronto para receber requisições. Use health checks:

services:
  db:
    image: postgres:15-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    # ...

  api:
    depends_on:
      db:
        condition: service_healthy
    # ...

Agora o api só inicia quando o banco estiver respondendo a requisições.

Fluxo de Trabalho Prático

Iniciando e Desenvolvendo

Para começar o desenvolvimento:

docker-compose up

Adicione -d para rodar em background:

docker-compose up -d

Para visualizar logs:

docker-compose logs -f api

O -f mantém o output em tempo real, como tail -f.

Quando você edita um arquivo no seu editor local, a mudança é refletida instantaneamente no container. Se estiver usando Nodemon ou similar, o serviço será automaticamente reiniciado.

Entrando no Container para Debug

Às vezes você precisa executar comandos dentro do container:

docker-compose exec api sh
# Agora você está dentro do container
npm test
npm run build

Para Python:

docker-compose exec app python manage.py shell

Destruindo e Limpando

Quando terminar:

docker-compose down

Isso para os containers mas preserva volumes nomeados. Para remover tudo, incluindo volumes:

docker-compose down -v

Conclusão

Aprendemos que Docker Compose é muito mais do que uma ferramenta para produção — é um multiplicador de produtividade em desenvolvimento. Os três pontos principais que você deve levar adiante:

  1. Volumes com bind mounts criam a ilusão de desenvolvimento local: O hot reload com Nodemon, Flask ou equivalentes transforma o Docker de um obstáculo em um diferencial, garantindo que você desenvolva na mesma plataforma que rodará em produção.

  2. Ambientes reproduzíveis eliminam surpresas: Versionar imagens específicas, usar lock files, e manter docker-compose.yml no Git garante que "funciona no meu computador" se converta em "funciona em qualquer lugar". Um novo desenvolvedor faz git clone, docker-compose up, e já está desenvolvendo.

  3. Multi-container orchestration simplifica arquiteturas complexas: Em vez de gerenciar PostgreSQL, Redis e sua API separadamente, o Compose centraliza tudo em um arquivo declarativo, com networking e health checks automáticos, economizando horas de setup.

Referências


Artigos relacionados