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:
- Verifique se o banco de dados iniciou corretamente:
docker-compose logs postgres - Teste a conectividade dentro do container:
docker-compose exec app ping postgres - 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.