DevOps Admin

Boas Práticas de Docker Compose: Ambientes Multi-container e Variáveis de Ambiente para Times Ágeis Já leu

Docker Compose: Ambientes Multi-container e Variáveis de Ambiente Docker Compose é uma ferramenta que permite definir e executar aplicações multi-container de forma declarativa através de um arquivo YAML. Em vez de executar vários comandos manualmente, você descreve toda a infraestrutura em um único arquivo, facilitando o desenvolvimento, testes e deploy. Isso é especialmente útil quando sua aplicação depende de vários serviços: banco de dados, cache, fila de mensagens, API, etc. O principal benefício é a reprodutibilidade. Qualquer desenvolvedor pode clonar seu repositório, executar e ter exatamente o mesmo ambiente rodando localmente. Isso elimina aquele clássico problema: "funciona na minha máquina" — agora funciona em todas as máquinas que têm Docker instalado. Instalação e Primeiros Passos Se você já tem Docker Desktop instalado (Windows ou macOS), Docker Compose já vem incluído. No Linux, você precisará instalar separadamente. Verifique a versão com: A estrutura básica de um projeto com Compose é simples: você cria um arquivo chamado na raiz do projeto. Dentro

Docker Compose: Ambientes Multi-container e Variáveis de Ambiente

Docker Compose é uma ferramenta que permite definir e executar aplicações multi-container de forma declarativa através de um arquivo YAML. Em vez de executar vários comandos docker run manualmente, você descreve toda a infraestrutura em um único arquivo, facilitando o desenvolvimento, testes e deploy. Isso é especialmente útil quando sua aplicação depende de vários serviços: banco de dados, cache, fila de mensagens, API, etc.

O principal benefício é a reprodutibilidade. Qualquer desenvolvedor pode clonar seu repositório, executar docker-compose up e ter exatamente o mesmo ambiente rodando localmente. Isso elimina aquele clássico problema: "funciona na minha máquina" — agora funciona em todas as máquinas que têm Docker instalado.

Instalação e Primeiros Passos

Se você já tem Docker Desktop instalado (Windows ou macOS), Docker Compose já vem incluído. No Linux, você precisará instalar separadamente. Verifique a versão com:

docker-compose --version

A estrutura básica de um projeto com Compose é simples: você cria um arquivo chamado docker-compose.yml na raiz do projeto. Dentro dele, você define quais serviços (containers) sua aplicação precisa e como eles devem ser configurados.

Estrutura e Sintaxe do docker-compose.yml

O arquivo docker-compose.yml usa a versão YAML e organiza-se em seções principais. Vamos entender cada uma antes de ver exemplos práticos.

Versão e Estrutura Básica

version: '3.9'

services:
  web:
    image: nginx:latest
    ports:
      - "80:80"

  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: senha123

A versão 3.9 é uma das mais estáveis e compatíveis atualmente. A seção services é onde você define cada container que sua aplicação precisa. Cada serviço recebe um nome (neste caso, web e db) que se torna o hostname para comunicação entre containers.

Configurações Essenciais de um Serviço

Cada serviço pode ter múltiplas configurações. As mais importantes são: image (qual imagem Docker usar), ports (mapeamento de portas), volumes (persistência de dados), environment (variáveis de ambiente) e depends_on (dependências entre serviços).

version: '3.9'

services:
  app:
    build: .
    container_name: minha_app
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DEBUG=true
    depends_on:
      - database
    command: npm start
    restart: unless-stopped

  database:
    image: postgres:15-alpine
    container_name: postgres_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: senha_segura
      POSTGRES_DB: minha_app_db

volumes:
  postgres_data:

Note que build: . instrui o Docker Compose a construir a imagem a partir do Dockerfile no diretório atual, enquanto image: usa uma imagem pré-existente. O depends_on garante que o banco de dados inicie antes da aplicação. A seção volumes ao final define volumes nomeados que persistem dados mesmo após os containers serem destruídos.

Variáveis de Ambiente: Configuração Dinâmica

Variáveis de ambiente são fundamentais para que sua aplicação funcione em diferentes contextos (desenvolvimento, teste, produção) sem alterar o código. Docker Compose oferece várias maneiras de gerenciá-las.

Métodos de Definir Variáveis

O primeiro método é direto no docker-compose.yml usando a chave environment. Você pode defini-las como uma lista ou como um dicionário:

version: '3.9'

services:
  api:
    image: node:18-alpine
    environment:
      - DATABASE_HOST=database
      - DATABASE_PORT=5432
      - DATABASE_USER=app_user
      - LOG_LEVEL=info

Porém, colocar valores sensíveis (senhas, tokens) diretamente no docker-compose.yml é uma má prática de segurança, especialmente se você versionará esse arquivo no Git. O segundo método usa um arquivo .env:

# .env
DATABASE_PASSWORD=senha_super_secreta
API_TOKEN=abc123xyz789
REDIS_HOST=redis
DEBUG=false

No docker-compose.yml, você referencia essas variáveis com ${NOME_DA_VARIAVEL}:

version: '3.9'

services:
  app:
    image: app:latest
    environment:
      - DATABASE_PASSWORD=${DATABASE_PASSWORD}
      - API_TOKEN=${API_TOKEN}
      - REDIS_HOST=${REDIS_HOST}
      - DEBUG=${DEBUG}

  database:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}

Docker Compose carrega automaticamente variáveis do arquivo .env se ele estiver no mesmo diretório do docker-compose.yml. Lembre-se: adicione .env ao seu .gitignore para não expor dados sensíveis no repositório.

Arquivo .env para Diferentes Ambientes

Para ambientes diferentes, você pode usar múltiplos arquivos .env e especificá-los com a flag --env-file:

docker-compose --env-file .env.development up
docker-compose --env-file .env.production up

Ou simplesmente renomeie os arquivos e use o padrão:

# docker-compose.yml
version: '3.9'

services:
  web:
    image: app:latest
    environment:
      - APP_ENV=${APP_ENV:-development}
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}

A sintaxe ${VARIAVEL:-valor_padrao} define um valor padrão caso a variável não esteja definida. Isso torna seu Compose mais robusto e menos propenso a erros.

Exemplo Prático Completo: Stack Web com PostgreSQL, Redis e Node.js

Vamos criar um exemplo real funcionando. Este projeto terá uma aplicação Node.js, um banco de dados PostgreSQL e um cache Redis.

Estrutura de Pastas

projeto/
├── docker-compose.yml
├── .env
├── .env.example
├── Dockerfile
├── src/
│   ├── index.js
│   ├── package.json
│   └── package-lock.json
└── .gitignore

Arquivo .env.example (para documentação)

# Copie este arquivo para .env e preencha os valores
APP_ENV=development
NODE_ENV=development
PORT=3000

# Database
DB_HOST=postgres
DB_PORT=5432
DB_USER=app_user
DB_PASSWORD=senha_dev_123
DB_NAME=app_database

# Redis
REDIS_HOST=redis
REDIS_PORT=6379

Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

docker-compose.yml

version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app_web
    ports:
      - "${PORT:-3000}:3000"
    environment:
      - NODE_ENV=${NODE_ENV}
      - APP_ENV=${APP_ENV}
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
      - REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
      - LOG_LEVEL=debug
    volumes:
      - ./src:/app/src
      - /app/node_modules
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    networks:
      - app-network

  postgres:
    image: postgres:15-alpine
    container_name: app_postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: app_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - app-network

volumes:
  postgres_data:
  redis_data:

networks:
  app-network:
    driver: bridge

Arquivo src/index.js (Exemplo de Aplicação)

const express = require('express');
const { Pool } = require('pg');
const redis = require('redis');

const app = express();
const port = process.env.PORT || 3000;

// Conexão PostgreSQL
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// Conexão Redis
const redisClient = redis.createClient({
  url: process.env.REDIS_URL,
});

redisClient.on('error', (err) => console.log('Redis Error:', err));
redisClient.connect();

app.get('/health', (req, res) => {
  res.json({ status: 'ok', environment: process.env.APP_ENV });
});

app.get('/users', async (req, res) => {
  try {
    const cached = await redisClient.get('users');
    if (cached) {
      return res.json({ source: 'cache', data: JSON.parse(cached) });
    }

    const result = await pool.query('SELECT * FROM users LIMIT 10');
    await redisClient.setEx('users', 3600, JSON.stringify(result.rows));

    res.json({ source: 'database', data: result.rows });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(port, () => {
  console.log(`App rodando em http://localhost:${port}`);
  console.log(`Ambiente: ${process.env.APP_ENV}`);
});

package.json

{
  "name": "app",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pg": "^8.10.0",
    "redis": "^4.6.7"
  }
}

Comandos para Executar

# Criar arquivo .env a partir do exemplo
cp .env.example .env

# Compilar imagens e iniciar todos os containers
docker-compose up -d

# Verificar status
docker-compose ps

# Ver logs da aplicação
docker-compose logs -f app

# Executar comando dentro de um container
docker-compose exec app npm install

# Parar todos os containers
docker-compose down

# Parar e remover volumes (cuidado!)
docker-compose down -v

Boas Práticas e Troubleshooting

Segurança

Nunca commite o arquivo .env com dados reais no repositório. Use .env.example como template. Para produção, use secrets management como Docker Secrets (em Swarm) ou use variáveis de ambiente do seu orquestrador (Kubernetes, por exemplo).

# .gitignore
.env
.env.local
*.log

Healthchecks

Use healthcheck para garantir que os containers estão genuinamente prontos antes de iniciar dependências. No exemplo anterior, o PostgreSQL só é considerado "ready" quando responde ao comando pg_isready.

Isolamento de Rede

Defina uma rede customizada (app-network no exemplo) para seus containers. Isso oferece melhor isolamento e permite que os serviços se comuniquem pelo nome do serviço sem expor portas desnecessariamente.

Debugging Comum

Se sua aplicação não consegue conectar ao banco de dados:

  1. Verifique se o banco de dados iniciou corretamente: docker-compose logs postgres
  2. Teste a conectividade dentro do container: docker-compose exec app ping postgres
  3. Confirme se as variáveis de ambiente estão corretas: docker-compose exec app env | grep DATABASE

Conclusão

Você aprendeu que Docker Compose transforma a complexidade de ambientes multi-container em um arquivo YAML simples e versionável, eliminando problemas de reprodutibilidade. O segundo ponto essencial é que variáveis de ambiente são o mecanismo correto para configuração dinâmica, permitindo que o mesmo código rode em desenvolvimento, teste e produção sem alterações. Por fim, healthchecks, redes customizadas e volumes nomeados são práticas que transformam seu Compose de um experimento local em um setup robusto, seguro e escalável.

Referências


Artigos relacionados