DevOps & CI/CD

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

13 min de leitura

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

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

<h2>Docker Compose: Ambientes Multi-container e Variáveis de Ambiente</h2>

<p>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 <code>docker run</code> 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.</p>

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

<h3>Instalação e Primeiros Passos</h3>

<p>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:</p>

<pre><code class="language-bash">docker-compose --version</code></pre>

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

<h2>Estrutura e Sintaxe do docker-compose.yml</h2>

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

<h3>Versão e Estrutura Básica</h3>

<pre><code class="language-yaml">version: &#039;3.9&#039;

services:

web:

image: nginx:latest

ports:

  • &quot;80:80&quot;

db:

image: postgres:15

environment:

POSTGRES_PASSWORD: senha123</code></pre>

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

<h3>Configurações Essenciais de um Serviço</h3>

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

<pre><code class="language-yaml">version: &#039;3.9&#039;

services:

app:

build: .

container_name: minha_app

ports:

  • &quot;3000:3000&quot;

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:

  • &quot;5432:5432&quot;

volumes:

  • postgres_data:/var/lib/postgresql/data

environment:

POSTGRES_USER: admin

POSTGRES_PASSWORD: senha_segura

POSTGRES_DB: minha_app_db

volumes:

postgres_data:</code></pre>

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

<h2>Variáveis de Ambiente: Configuração Dinâmica</h2>

<p>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.</p>

<h3>Métodos de Definir Variáveis</h3>

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

<pre><code class="language-yaml">version: &#039;3.9&#039;

services:

api:

image: node:18-alpine

environment:

  • DATABASE_HOST=database
  • DATABASE_PORT=5432
  • DATABASE_USER=app_user
  • LOG_LEVEL=info</code></pre>

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

<pre><code class="language-bash"># .env

DATABASE_PASSWORD=senha_super_secreta

API_TOKEN=abc123xyz789

REDIS_HOST=redis

DEBUG=false</code></pre>

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

<pre><code class="language-yaml">version: &#039;3.9&#039;

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}</code></pre>

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

<h3>Arquivo .env para Diferentes Ambientes</h3>

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

<pre><code class="language-bash">docker-compose --env-file .env.development up

docker-compose --env-file .env.production up</code></pre>

<p>Ou simplesmente renomeie os arquivos e use o padrão:</p>

<pre><code class="language-yaml"># docker-compose.yml

version: &#039;3.9&#039;

services:

web:

image: app:latest

environment:

  • APP_ENV=${APP_ENV:-development}
  • DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}</code></pre>

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

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

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

<h3>Estrutura de Pastas</h3>

<pre><code>projeto/

├── docker-compose.yml

├── .env

├── .env.example

├── Dockerfile

├── src/

│ ├── index.js

│ ├── package.json

│ └── package-lock.json

└── .gitignore</code></pre>

<h3>Arquivo .env.example (para documentação)</h3>

<pre><code class="language-bash"># 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</code></pre>

<h3>Dockerfile</h3>

<pre><code class="language-dockerfile">FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD [&quot;npm&quot;, &quot;start&quot;]</code></pre>

<h3>docker-compose.yml</h3>

<pre><code class="language-yaml">version: &#039;3.9&#039;

services:

app:

build:

context: .

dockerfile: Dockerfile

container_name: app_web

ports:

  • &quot;${PORT:-3000}:3000&quot;

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:

  • &quot;5432:5432&quot;

environment:

POSTGRES_USER: ${DB_USER}

POSTGRES_PASSWORD: ${DB_PASSWORD}

POSTGRES_DB: ${DB_NAME}

volumes:

  • postgres_data:/var/lib/postgresql/data

healthcheck:

test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U ${DB_USER}&quot;]

interval: 10s

timeout: 5s

retries: 5

restart: unless-stopped

networks:

  • app-network

redis:

image: redis:7-alpine

container_name: app_redis

ports:

  • &quot;6379:6379&quot;

volumes:

  • redis_data:/data

restart: unless-stopped

networks:

  • app-network

volumes:

postgres_data:

redis_data:

networks:

app-network:

driver: bridge</code></pre>

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

<pre><code class="language-javascript">const express = require(&#039;express&#039;);

const { Pool } = require(&#039;pg&#039;);

const redis = require(&#039;redis&#039;);

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(&#039;error&#039;, (err) =&gt; console.log(&#039;Redis Error:&#039;, err));

redisClient.connect();

app.get(&#039;/health&#039;, (req, res) =&gt; {

res.json({ status: &#039;ok&#039;, environment: process.env.APP_ENV });

});

app.get(&#039;/users&#039;, async (req, res) =&gt; {

try {

const cached = await redisClient.get(&#039;users&#039;);

if (cached) {

return res.json({ source: &#039;cache&#039;, data: JSON.parse(cached) });

}

const result = await pool.query(&#039;SELECT * FROM users LIMIT 10&#039;);

await redisClient.setEx(&#039;users&#039;, 3600, JSON.stringify(result.rows));

res.json({ source: &#039;database&#039;, data: result.rows });

} catch (err) {

res.status(500).json({ error: err.message });

}

});

app.listen(port, () =&gt; {

console.log(App rodando em http://localhost:${port});

console.log(Ambiente: ${process.env.APP_ENV});

});</code></pre>

<h3>package.json</h3>

<pre><code class="language-json">{

&quot;name&quot;: &quot;app&quot;,

&quot;version&quot;: &quot;1.0.0&quot;,

&quot;main&quot;: &quot;src/index.js&quot;,

&quot;scripts&quot;: {

&quot;start&quot;: &quot;node src/index.js&quot;,

&quot;dev&quot;: &quot;nodemon src/index.js&quot;

},

&quot;dependencies&quot;: {

&quot;express&quot;: &quot;^4.18.2&quot;,

&quot;pg&quot;: &quot;^8.10.0&quot;,

&quot;redis&quot;: &quot;^4.6.7&quot;

}

}</code></pre>

<h3>Comandos para Executar</h3>

<pre><code class="language-bash"># 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</code></pre>

<h2>Boas Práticas e Troubleshooting</h2>

<h3>Segurança</h3>

<p>Nunca commite o arquivo <code>.env</code> com dados reais no repositório. Use <code>.env.example</code> 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).</p>

<pre><code class="language-bash"># .gitignore

.env

.env.local

*.log</code></pre>

<h3>Healthchecks</h3>

<p>Use <code>healthcheck</code> para garantir que os containers estão genuinamente prontos antes de iniciar dependências. No exemplo anterior, o PostgreSQL só é considerado &quot;ready&quot; quando responde ao comando <code>pg_isready</code>.</p>

<h3>Isolamento de Rede</h3>

<p>Defina uma rede customizada (<code>app-network</code> 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.</p>

<h3>Debugging Comum</h3>

<p>Se sua aplicação não consegue conectar ao banco de dados:</p>

<ol>

<li>Verifique se o banco de dados iniciou corretamente: <code>docker-compose logs postgres</code></li>

<li>Teste a conectividade dentro do container: <code>docker-compose exec app ping postgres</code></li>

</ol>

<p>3. Confirme se as variáveis de ambiente estão corretas: <code>docker-compose exec app env | grep DATABASE</code></p>

<h2>Conclusão</h2>

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

<h2>Referências</h2>

<ul>

<li><a href="https://docs.docker.com/compose/" target="_blank" rel="noopener noreferrer">Docker Compose Official Documentation</a></li>

<li><a href="https://docs.docker.com/compose/compose-file/" target="_blank" rel="noopener noreferrer">Docker Compose File Format Reference</a></li>

<li><a href="https://docs.docker.com/compose/environment-variables/" target="_blank" rel="noopener noreferrer">Environment Variables in Compose</a></li>

<li><a href="https://docs.docker.com/network/" target="_blank" rel="noopener noreferrer">Docker Networking Guide</a></li>

<li><a href="https://hub.docker.com/_/postgres" target="_blank" rel="noopener noreferrer">PostgreSQL Docker Image Documentation</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

Como Usar Gerenciamento de Processos no Linux: systemd, journald e Serviços em Produção
Como Usar Gerenciamento de Processos no Linux: systemd, journald e Serviços em Produção

Entendendo o systemd: O Coração do Linux Moderno O systemd é o sistema de ini...

Guia Completo de SRE na Prática: SLIs, SLOs, SLAs e Error Budgets
Guia Completo de SRE na Prática: SLIs, SLOs, SLAs e Error Budgets

SRE: O Fundamento para Confiabilidade em Produção Site Reliability Engineerin...

Dominando Tekton em Kubernetes: Pipelines Cloud-Native do Zero em Projetos Reais
Dominando Tekton em Kubernetes: Pipelines Cloud-Native do Zero em Projetos Reais

O que é Tekton e por que você precisa aprender Tekton é um framework open-sou...