Skip to main content

Welcome to a Grasp lesson

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

Lesson 29

Managing Service Startup Order with 'depends_on'

Recommended resources

Olá! Bem-vindo de volta.

Introdução

Na lição anterior, você aprendeu a desacoplar a configuração da sua aplicação usando variáveis de ambiente e arquivos .env. Isso nos permitiu criar serviços flexíveis. No entanto, em uma aplicação com múltiplos serviços, como uma API .NET e um banco de dados PostgreSQL, a ordem em que os serviços iniciam é crucial. Se a sua API iniciar e tentar se conectar ao banco de dados antes que ele esteja pronto para aceitar conexões, sua aplicação irá falhar.

Nesta lição, vamos resolver exatamente esse problema. Você aprenderá a controlar a ordem de inicialização e as dependências entre os serviços usando a propriedade depends_on no Docker Compose. Compreender isso é fundamental para construir aplicações multi-contêiner robustas e resilientes.

Ao final desta lição, você será capaz de orquestrar a inicialização dos seus serviços de forma que a sua API só inicie quando o banco de dados do qual ela depende estiver verdadeiramente pronto.


O Problema: Condições de Corrida na Inicialização

Quando você executa docker-compose up sem nenhuma diretiva de dependência, o Docker Compose inicia todos os serviços definidos no arquivo, mas não garante nenhuma ordem específica. Eles iniciam de forma mais ou menos simultânea.

Isso cria uma "condição de corrida" (race condition). Considere o seguinte cenário:

  • Serviço webapi: Uma API .NET que precisa de uma conexão com o banco de dados ao iniciar.
  • Serviço db: Um contêiner PostgreSQL.

O contêiner do webapi pode iniciar e executar seu código de inicialização (por exemplo, em Program.cs) mais rápido do que o contêiner do db leva para inicializar o processo do PostgreSQL e começar a ouvir na porta 5432. O resultado? A API tenta se conectar, a conexão falha, e a aplicação provavelmente trava na inicialização.


A Solução Inicial: depends_on Básico

A primeira solução que o Docker Compose oferece é a propriedade depends_on. Na sua forma mais simples, ela diz ao Docker Compose para iniciar um contêiner antes de outro.

Docker Compose Workflow with Service Dependencies
Este diagrama ilustra como o Docker Compose usa o arquivo `docker-compose.yml` para orquestrar serviços. A propriedade `depends_on` no serviço `WebApi` garante que o serviço `SqlServerDb` seja iniciado primeiro.

Vamos ver um exemplo com nossa API e o banco de dados:

version: '3.8'

services:
  webapi:
    image: my-dotnet-api
    ports:
      - "8080:80"
    environment:
      - ConnectionStrings__DefaultConnection=Host=db;... # string de conexão
    depends_on:
      - db # Diz ao Compose para iniciar 'db' antes de 'webapi'

  db:
    image: postgres:14
    environment:
      - POSTGRES_PASSWORD=mysecretpassword

Neste caso, depends_on: - db garante que o contêiner db será iniciado antes do contêiner webapi.

Docker Compose will BLOW your MIND!! (a tutorial)

Para ver uma demonstração prática do depends_on básico, assista a este segmento do vídeo do canal NetworkChuck. Ele mostra como usar a propriedade para garantir que um banco de dados MySQL seja criado antes de um site WordPress que depende dele.

Assista do timestamp 10:46 a 12:53. Preste atenção em como a seção depends_on é adicionada ao serviço WordPress para criar uma dependência no serviço mysql.

A Limitação Crucial do depends_on Básico

Embora o depends_on básico garanta a ordem de inicialização dos contêineres, ele não espera que a aplicação dentro do contêiner esteja pronta. Ele apenas espera o contêiner estar no estado "running". Para um banco de dados, o contêiner pode estar "running", mas o processo do PostgreSQL ainda pode estar realizando a inicialização, que pode levar vários segundos. A condição de corrida ainda existe, embora a janela de tempo seja menor.

How to Use Docker Compose depends_on with Health Checks

Para entender completamente essa limitação, leia a introdução deste artigo do blog OneUptime. Ele articula claramente por que o depends_on básico é insuficiente para dependências de serviço reais.

Leia a seção introdutória e a subseção "The Problem with Basic depends_on". O artigo explica de forma sucinta por que esperar que um contêiner inicie não é o mesmo que esperar que o serviço dentro dele esteja saudável e pronto.


A Solução Robusta: depends_on com Condições e Health Checks

Para resolver de vez o problema, as versões mais recentes do Docker Compose expandiram o depends_on para aceitar condições que estão ligadas a health checks (verificações de saúde).

Um healthcheck é um comando que o Docker executa periodicamente dentro de um contêiner para verificar se a aplicação está funcionando corretamente.

Docker Compose `depends_on` and `healthcheck` Configuration
Esta imagem mostra a sintaxe moderna: o `depends_on` agora usa uma condição `service_healthy`, que está diretamente ligada à diretiva `healthcheck` definida no serviço de banco de dados.

A sintaxe longa e moderna se parece com isso:

version: '3.8'

services:
  webapi:
    image: my-dotnet-api
    depends_on:
      db: # Nome do serviço dependente
        condition: service_healthy # A condição a ser esperada

  db:
    image: postgres:14
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mysecretpassword
    healthcheck:
      # Comando para verificar se o Postgres está pronto para aceitar conexões
      test: ["CMD-SHELL", "pg_isready -U myuser"]
      interval: 5s
      timeout: 5s
      retries: 5

Agora, o serviço webapi só iniciará quando o comando do healthcheck do serviço db (pg_isready) retornar sucesso, indicando que o banco de dados está verdadeiramente pronto.

Existem três condições principais que você pode usar:

Condição Comportamento
service_started Espera que o contêiner da dependência seja iniciado. (Comportamento padrão/antigo)
service_healthy Espera que a dependência seja iniciada e passe no seu healthcheck.
service_completed_successfully Espera que a dependência seja executada até o fim e saia com o código 0 (sucesso).

Use Case: Migrações de Banco de Dados com service_completed_successfully

A condição service_completed_successfully é extremamente útil para tarefas pontuais que devem ser concluídas antes que a aplicação principal inicie. O exemplo mais comum para um desenvolvedor é a execução de migrações de banco de dados (por exemplo, com Entity Framework).

Veja como seria a orquestração:

  1. O serviço de banco de dados (db) inicia.
  2. O serviço de migração (db-migrator) espera o db ficar saudável (service_healthy).
  3. O db-migrator executa as migrações (ex: dotnet ef database update) e então termina com sucesso.
  4. A aplicação principal (webapi) espera o db-migrator terminar com sucesso (service_completed_successfully) antes de iniciar.
services:
  webapi:
    image: my-dotnet-api
    depends_on:
      db-migrator:
        condition: service_completed_successfully
  
  db-migrator:
    image: my-dotnet-api # Usa a mesma imagem da API
    command: ["dotnet", "ef", "database", "update"] # Sobrescreve o comando para rodar a migração
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:14
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser"]
      # ... resto da configuração

How to Use Docker Compose depends_on with Health Checks

Para aprofundar seu conhecimento sobre as condições do depends_on e ver mais exemplos práticos, incluindo health checks para serviços comuns como Redis e PostgreSQL, consulte este artigo novamente.

Leia as seções "depends_on with Conditions" e "Common Service Health Checks". Preste atenção nos diferentes exemplos de condições e nos comandos de health check prontos para serviços como PostgreSQL e Redis. Isso lhe dará uma base sólida para a próxima lição, onde você implementará seus próprios health checks.

Teste seu entendimento!

Você tem a seguinte aplicação docker-compose.yml com três serviços:

  • api: a API principal.
  • cache: um serviço de cache Redis.
  • init-script: um script que precisa rodar uma única vez para popular alguns dados iniciais no cache.

O init-script deve ser executado somente depois que o cache (Redis) estiver pronto. A api só deve iniciar depois que o init-script for concluído com sucesso.

Como você configuraria as seções depends_on para os serviços api e init-script para garantir essa ordem de inicialização? Assuma que o serviço cache já tem um healthcheck definido.

version: '3.8'

services:
  api:
    image: my-api
    # ...
    # TODO: Adicionar a seção depends_on para a API
    # ...

  init-script:
    image: my-init-script
    # ...
    # TODO: Adicionar a seção depends_on para o script de inicialização
    # ...

  cache:
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 1s
      timeout: 3s
      retries: 30
Show answer

A configuração correta seria:

version: '3.8'

services:
  api:
    image: my-api
    depends_on:
      init-script:
        condition: service_completed_successfully

  init-script:
    image: my-init-script
    depends_on:
      cache:
        condition: service_healthy

  cache:
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 1s
      timeout: 3s
      retries: 30

Explicação:

  1. O init-script depende que o serviço cache esteja saudável (service_healthy), garantindo que o Redis esteja pronto para aceitar comandos.
  2. A api depende que o init-script tenha sido concluído com sucesso (service_completed_successfully), garantindo que os dados iniciais estejam no cache antes que a API comece a receber requisições.

Conclusão

Parabéns! Você agora entende como orquestrar a inicialização de serviços em uma aplicação multi-contêiner de forma confiável. Você aprendeu a ir além da simples ordem de inicialização e a garantir que os serviços estejam verdadeiramente prontos antes que outros que dependem deles iniciem.

Principais Pontos:

  • Condições de Corrida: Iniciar serviços sem uma ordem definida pode fazer com que aplicações falhem se uma dependência não estiver pronta.
  • depends_on Básico: Garante a ordem de inicialização dos contêineres, mas não a prontidão da aplicação dentro deles. É um mecanismo insuficiente para a maioria dos casos de uso do mundo real.
  • depends_on com Condições: A abordagem moderna e robusta.
    • condition: service_healthy: Espera que um serviço passe em sua verificação de saúde (healthcheck). É a escolha ideal para dependências de longa duração, como bancos de dados e outras APIs.
    • condition: service_completed_successfully: Espera que um serviço execute sua tarefa e termine com sucesso. Perfeito para tarefas de inicialização, como migrações de banco de dados ou scripts de seeding.

Na próxima lição, vamos nos aprofundar no que torna tudo isso possível: implementar health checks em Docker Compose. Você aprenderá a escrever seus próprios healthchecks para seus serviços .NET, dando a você controle total sobre a definição de "pronto" para suas aplicações.