Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Docker Compose: Managing Environment Variables

Recommended resources

Olá! Bem-vindo de volta.

Introdução

Na lição anterior, você aprendeu a gerenciar o ciclo de vida de uma aplicação multi-contêiner usando os comandos docker-compose up, down, e logs. Agora que você sabe como orquestrar seus serviços, o próximo passo lógico é aprender a configurá-los de forma flexível e segura.

Nesta lição, vamos nos concentrar em como fornecer variáveis de ambiente para seus serviços usando a chave environment e arquivos .env no Docker Compose. Este é um conceito fundamental no desenvolvimento de software moderno, pois permite desacoplar a configuração da sua aplicação do código e da imagem do contêiner. Para você, como desenvolvedor .NET, isso se alinha perfeitamente com a forma como o ASP.NET Core gerencia a configuração, lendo valores de fontes como arquivos appsettings.json e, crucialmente, variáveis de ambiente.

Ao final desta lição, você será capaz de construir aplicações Docker Compose que podem ser facilmente adaptadas para diferentes ambientes (desenvolvimento, teste, produção) simplesmente alterando a configuração, sem precisar reconstruir nenhuma imagem.


Por que Usar Variáveis de Ambiente?

Imagine que você tem uma API .NET que precisa de uma string de conexão para se comunicar com um banco de dados PostgreSQL. Uma abordagem ingênua seria colocar essa string de conexão diretamente no seu arquivo appsettings.json e copiá-la para a imagem Docker. O problema? Se você precisar apontar para um banco de dados diferente (por exemplo, de produção), terá que modificar o arquivo e reconstruir a imagem. Isso é ineficiente e viola o princípio de construir um artefato (a imagem Docker) que pode ser promovido através de diferentes ambientes.

As variáveis de ambiente resolvem isso. Elas permitem que você injete valores de configuração em um contêiner no momento em que ele é executado.

A imagem a seguir ilustra esse fluxo: a configuração é mantida fora do contêiner (em arquivos .env ou no seu shell) e o Docker Compose a injeta no ambiente do contêiner durante a inicialização.

Setting Environment Variables in Docker Compose
Este diagrama mostra como as variáveis de ambiente são gerenciadas no Docker Compose. A configuração, definida em um arquivo `.env` e referenciada no `docker-compose.yml`, é passada do Docker Host para o ambiente interno do Contêiner Docker no momento da execução.

Métodos para Fornecer Variáveis de Ambiente

O Docker Compose oferece várias maneiras de gerenciar variáveis de ambiente, cada uma com seu caso de uso específico.

1. A Chave environment

A maneira mais direta de definir uma variável de ambiente é usando a chave environment diretamente no serviço dentro do seu arquivo docker-compose.yml.

Você pode usar uma sintaxe de mapa (chave-valor) ou uma lista.

Sintaxe de Mapa:

services:
  webapi:
    image: my-aspnet-api
    environment:
      ASPNETCORE_ENVIRONMENT: "Development"
      Logging__LogLevel__Default: "Information"

Sintaxe de Lista:

services:
  webapi:
    image: my-aspnet-api
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - Logging__LogLevel__Default=Information

Ambas as formas são equivalentes. A sintaxe de mapa costuma ser mais legível.

Set environment variables

A documentação oficial do Docker oferece uma explicação concisa sobre o uso da chave environment. Recomendo a leitura para solidificar o conceito.

Leia a seção "Use the environment attribute". Preste atenção às duas sintaxes (mapa e lista) e como você pode usar a interpolação de variáveis, que veremos a seguir.

2. Arquivos .env para Externalizar a Configuração

Embora a chave environment seja útil, colocar valores sensíveis (como senhas de banco de dados) ou valores que mudam com frequência diretamente no docker-compose.yml não é uma boa prática. A melhor abordagem é externalizar esses valores para arquivos de ambiente.

a) O Arquivo Padrão .env

Por padrão, quando você executa docker-compose up, o Docker Compose procura por um arquivo chamado .env no mesmo diretório do seu docker-compose.yml. As variáveis definidas neste arquivo ficam disponíveis para substituição de variáveis dentro do próprio docker-compose.yml.

Crie um arquivo .env (observe o ponto no início):

# .env
TAG=1.0.0
WEB_PORT=8080

Agora, você pode usar essas variáveis no seu docker-compose.yml:

# docker-compose.yml
services:
  webapi:
    image: my-aspnet-api:${TAG} # Substituição de variável
    ports:
      - "${WEB_PORT}:80"

Neste caso, o Docker Compose substituirá ${TAG} por 1.0.0 e ${WEB_PORT} por 8080 antes de executar o serviço. Isso é extremamente poderoso para parametrizar suas definições.

Docker Environment Variables: Best Practices and Examples

Para ver a substituição de variáveis em ação, assista a este vídeo do canal Code with Nevyan. Ele demonstra como usar um arquivo .env para customizar dinamicamente nomes de contêineres e versões de imagens.

Assista ao vídeo do início até 1:36 e depois de 2:59 a 3:36. Observe como as variáveis definidas no arquivo .env são usadas com a sintaxe ${VARIABLE} dentro do docker-compose.yml para configurar dinamicamente a versão da imagem, mapeamentos de volume e prefixos de nome de contêiner.

b) A Chave env_file

E se você quiser passar o conteúdo de um arquivo de ambiente diretamente para dentro de um contêiner? Para isso, usamos a chave env_file. Isso é ideal para agrupar todas as variáveis de ambiente de um serviço específico em um único arquivo.

Crie um arquivo, por exemplo, database.env:

# database.env
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DB=mydatabase

Importante: Adicione este arquivo ao seu .gitignore para não cometer credenciais ao seu repositório!

Agora, use env_file no seu serviço de banco de dados:

# docker-compose.yml
services:
  db:
    image: postgres:14
    env_file:
      - database.env
    volumes:
      - postgres-data:/var/lib/postgresql/data
volumes:
  postgres-data:

Neste caso, o contêiner db será iniciado com as variáveis POSTGRES_USER, POSTGRES_PASSWORD, e POSTGRES_DB definidas em seu ambiente interno.

Docker Compose Tutorial for Beginners (Networks - Volumes - Secrets - Postgres - Letsencrypt)

O vídeo a seguir, do canal Anton Putra, demonstra claramente o uso da chave env_file para separar configurações e segredos do arquivo Compose principal.

Assista do timestamp 26:00 a 28:17. O vídeo mostra como mover uma variável hardcoded para um arquivo de ambiente e usar a diretiva env_file para injetá-la no contêiner. Note a ênfase em não cometer o arquivo de ambiente ao Git.


Juntando Tudo: Exemplo Prático com .NET API e PostgreSQL

Vamos combinar esses conceitos em um cenário que você, como desenvolvedor .NET, encontrará com frequência. Queremos configurar a string de conexão de uma API para se conectar a um banco de dados Postgres, ambos rodando como contêineres.

  1. Crie o arquivo database.env com as credenciais do Postgres:

    # database.env
    POSTGRES_USER=myuser
    POSTGRES_PASSWORD=mysecretpassword
    POSTGRES_DB=mydatabase
    
  2. Crie o arquivo .env para variáveis de substituição:

    # .env
    API_PORT=5000
    
  3. Crie o docker-compose.yml:

    version: '3.8'
    
    services:
      db:
        image: postgres:14
        env_file:
          - database.env # Injeta as variáveis do arquivo no contêiner 'db'
        volumes:
          - postgres-data:/var/lib/postgresql/data
        networks:
          - my-network
    
      webapi:
        image: my-aspnet-api:latest
        ports:
          - "${API_PORT}:80" # Substitui a porta a partir do .env
        environment:
          # A string de conexão é construída usando variáveis injetadas no contêiner 'db'
          # O framework de configuração do .NET Core entende o formato ConnectionStrings__DefaultConnection
          ConnectionStrings__DefaultConnection: "Server=db;Port=5432;Database=${POSTGRES_DB};User Id=${POSTGRES_USER};Password=${POSTGRES_PASSWORD};"
        networks:
          - my-network
    
    volumes:
      postgres-data:
    
    networks:
      my-network:
    

Análise do Exemplo:

  • O serviço db lê suas credenciais diretamente do database.env através de env_file.
  • As variáveis em database.env também estão disponíveis para substituição no docker-compose.yml.
  • O serviço webapi usa a substituição de variáveis para definir sua string de conexão na seção environment. Ele constrói a string usando as variáveis definidas no database.env.
  • O nome do host na string de conexão é db, o nome do serviço Postgres. O Docker Compose fornece resolução DNS interna na rede customizada.

Ordem de Precedência

Uma pergunta natural é: o que acontece se uma variável for definida em vários lugares? Entender a ordem de precedência é fundamental para depurar configurações.

Use An .env File in Docker Compose

Este artigo do Warp.dev detalha excelentemente as várias maneiras de usar arquivos de ambiente e, mais importante, a ordem de precedência.

Leia as seções "Understanding the order of precedence of environment variables", "Passing environment variables to services", e "Passing .env files to services". Foque na lista de precedência e na nota sobre como variáveis na chave environment têm maior prioridade que as da env_file.

Resumindo a precedência, do mais alto para o mais baixo:

  1. Variáveis de ambiente definidas no seu shell (por exemplo, com export VAR=valor).
  2. Variáveis definidas na seção environment do seu docker-compose.yml.
  3. Variáveis definidas através da chave env_file.
  4. Variáveis definidas no arquivo .env padrão na raiz do projeto.

Isso significa que você pode definir valores padrão no seu .env, mas sobrescrevê-los para uma sessão específica exportando uma variável no seu terminal antes de rodar docker-compose up.

Teste seu entendimento!

Considere a seguinte configuração:

Arquivo .env:

LOG_LEVEL=INFO

Arquivo docker-compose.yml:

services:
  api:
    image: my-api
    environment:
      LOG_LEVEL: "DEBUG"
      API_KEY: "${API_KEY_FROM_SHELL}"

Antes de executar docker-compose up, você digita o seguinte comando no seu terminal:
export LOG_LEVEL=WARN
export API_KEY_FROM_SHELL=xyz123

Qual será o valor final da variável de ambiente LOG_LEVEL dentro do contêiner api? E o valor de API_KEY?

Show answer
  • LOG_LEVEL será "DEBUG". A variável definida diretamente na chave environment do docker-compose.yml tem maior precedência sobre a variável do shell e a do arquivo .env para o ambiente interno do contêiner.

  • API_KEY será "xyz123". Aqui, estamos lidando com substituição de variáveis. O valor de ${API_KEY_FROM_SHELL} é resolvido pelo Docker Compose antes de criar o contêiner. A variável do shell (export ...) tem a maior precedência para substituição, então seu valor é usado.

Este exercício destaca a sutil, mas importante, diferença entre definir uma variável para substituição no Compose e definir uma variável para o ambiente interno do contêiner.


Conclusão

Parabéns! Você agora sabe como configurar seus serviços de forma robusta e flexível usando variáveis de ambiente no Docker Compose. Esta é uma habilidade crucial para criar aplicações portáteis que se adaptam a diferentes ambientes sem modificação do código ou da imagem.

Principais Pontos:

  • Desacoplamento: Use variáveis de ambiente para separar a configuração (strings de conexão, chaves de API, portas) do seu código.
  • environment: A chave para definir variáveis diretamente no docker-compose.yml. Útil para valores não sensíveis e específicos do serviço.
  • .env e Substituição: O arquivo .env na raiz do projeto é usado para substituição de variáveis (${VAR}) dentro do docker-compose.yml, permitindo parametrizar suas definições.
  • env_file: A chave para injetar todas as variáveis de um arquivo diretamente no ambiente de um contêiner. Ideal para agrupar configurações de um serviço.
  • Precedência: Entender a ordem em que as variáveis são aplicadas (Shell > environment > env_file > .env) é essencial para a depuração.

Na próxima lição, abordaremos outro aspecto crucial da orquestração de serviços: controlar a ordem de inicialização e as dependências entre os serviços usando a propriedade depends_on. Isso garantirá, por exemplo, que seu banco de dados esteja pronto para aceitar conexões antes que sua API tente se conectar a ele.