Deploy: contêineres, nginx, Linux e a orquestração com Aspire

Deploy: contêineres, nginx, Linux e a orquestração com Aspire

A Livraria sai do computador de quem desenvolve: imagens de contêiner geradas pelo SDK, nginx na borda com TLS e WebSockets, migrations em bundle como passo do deploy, chaves de Data Protection e cabeçalhos encaminhados para réplicas, e o Aspire descrevendo o sistema inteiro em C#.
Linguagem C#

• • 20 min de leitura

A Livraria funciona no computador de quem desenvolve. Neste artigo ela vai para um servidor: imagens de contêiner geradas pelo SDK, um nginx na frente cuidando de TLS, migrations aplicadas no momento certo, a configuração que costuma ser esquecida, e o Aspire descrevendo o sistema inteiro em C#.

O que vai para produção

Contando os artigos anteriores, a Livraria tem nove peças em execução:

Peça Tipo Depende de
Livraria.Api ASP.NET Core PostgreSQL, Redis, RabbitMQ, Frete, Keycloak
Livraria.Frete gRPC nada
Livraria.Worker Worker Service RabbitMQ, Redis
Livraria.Site Razor Pages + Blazor API, Redis, Keycloak
PostgreSQL, Redis, RabbitMQ, Keycloak infraestrutura discos persistentes
nginx proxy reverso na borda o site e a API

Três perguntas guiam o artigo: como cada peça vira um artefato implantável, como elas se encontram em produção, e o que precisa ser configurado para que o comportamento em produção seja igual ao testado.

dotnet publish: do código ao artefato

dotnet build compila para desenvolvimento. dotnet publish produz o que vai para o servidor:

dotnet publish src/Livraria.Api -c Release -o ./publicado/api

As principais decisões de publicação:

  • Dependente do framework (padrão) ou autocontido (--self-contained). O primeiro exige o runtime .NET instalado no destino e gera poucos megabytes. O segundo leva o runtime junto, maior, e roda num servidor sem .NET instalado. Em contêineres, a imagem base já traz o runtime, e a publicação dependente do framework é a escolha natural.
  • RID (runtime identifier): -r linux-x64, linux-arm64, linux-musl-x64 (Alpine), win-x64. Necessário para publicações autocontidas e para Native AOT.
  • ReadyToRun (-p:PublishReadyToRun=true) pré-compila o IL para código nativo, e a aplicação arranca mais rápido. O JIT continua disponível e otimiza os pontos quentes depois, como vimos em "Do Código-Fonte ao Processo".
  • Native AOT, que "Performance e distribuição" vai tratar a fundo, gera um executável nativo sem JIT, com arranque em milissegundos. Exige código compatível com trimming, e as Minimal APIs com source generators foram desenhadas para isso.

Imagens de contêiner sem Dockerfile

Desde o .NET 8, o próprio SDK gera imagens de contêiner. Não é preciso Dockerfile, e nem o Docker é obrigatório: o comando abaixo, sem mais opções, entrega a imagem ao Docker local, mas com ContainerRegistry ela vai direto para um registro e com ContainerArchiveOutputPath vira um arquivo .tar.gz, os dois sem runtime de contêiner nenhum:

dotnet publish src/Livraria.Api -c Release --os linux --arch x64 /t:PublishContainer

As opções ficam no .csproj:

<PropertyGroup>
  <ContainerRepository>livraria/api</ContainerRepository>
  <ContainerImageTags>1.4.0;latest</ContainerImageTags>
  <ContainerFamily>noble-chiseled-extra</ContainerFamily>
</PropertyGroup>

A família chiseled merece atenção. São imagens Ubuntu "esculpidas" pela Canonical com a Microsoft, com só o que o runtime .NET precisa: sem shell, sem gerenciador de pacotes e rodando como usuário não root. A superfície de ataque é mínima, e um invasor que consiga executar código no processo não encontra bash, curl ou apt para continuar. A variante -extra inclui ICU e dados de fuso horário. Sem ela, a Livraria cairia no modo de globalização invariante contra o qual "Razor Pages" alertou: ToString("C") perderia o "R$", e TimeZoneInfo.FindSystemTimeZoneById("America/Sao_Paulo") lançaria exceção.

O Dockerfile clássico continua útil quando é preciso instalar algo na imagem, como fontes para geração de PDF ou uma biblioteca nativa. A forma recomendada é em múltiplos estágios:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/Livraria.Site -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["dotnet", "Livraria.Site.dll"]

O SDK, com compilador e ferramentas, só existe no primeiro estágio e não vai para a imagem final. As imagens oficiais escutam na porta 8080, como anunciado em "ASP.NET Core por dentro", porque um usuário sem privilégios não pode abrir portas abaixo de 1024.

Migrations: um passo do deploy, não da inicialização

A tentação é pôr await db.Database.MigrateAsync() no Program.cs. Com uma réplica funciona. Com três, as três tentam migrar ao mesmo tempo ao subir. Desde o EF Core 9, o Migrate pega uma trava no banco e as réplicas esperam a vez, sem corromper o esquema, mas cada uma fica parada até a anterior terminar, e a própria documentação continua recomendando aplicar as migrations no deploy, não na inicialização. Além disso, a aplicação passaria a rodar com uma conta de banco capaz de criar e apagar tabelas, que é mais privilégio do que ela precisa para atender pedidos.

O EF Core gera um bundle de migrations, um executável autocontido que aplica as migrations pendentes e termina:

dotnet ef migrations bundle --project src/Livraria.Dados.Postgres \
  --startup-project src/Livraria.Api -r linux-x64 --self-contained -o efbundle-postgres
./efbundle-postgres --connection "Host=db;Database=livraria;Username=livraria_migracao;Password=..."

O bundle roda uma vez, como passo do pipeline de deploy ou como um contêiner de inicialização que as réplicas esperam terminar, com uma conta de banco dedicada a alterações de esquema. A aplicação roda com uma conta que só lê e escreve dados. Para o MySQL há um bundle do projeto Livraria.Dados.MySql. O Mongo não tem migrations de esquema, e os índices são criados por um passo equivalente, um pequeno executável que chama CreateIndexesAsync.

Com várias réplicas, código novo e código velho convivem por alguns minutos durante uma atualização gradual. As migrations precisam ser compatíveis com a versão anterior do código, a mesma disciplina de contrato de "Validação, ProblemDetails e OpenAPI" aplicada ao esquema. Acrescentar uma coluna anulável é seguro. Renomear uma coluna exige três deploys: acrescentar a nova e escrever nas duas, migrar os dados e passar a ler da nova, remover a antiga.

A configuração que só aparece em produção

Algumas configurações não fazem diferença numa máquina de desenvolvimento, e são fatais com contêineres e réplicas.

Chaves de proteção de dados

Cookies de autenticação, tokens antiforgery e o TempData são criptografados pela Data Protection do ASP.NET Core, com chaves que por padrão ficam num diretório do sistema de arquivos local. Num contêiner, esse diretório some a cada recriação, ou seja, a cada deploy. Com três réplicas, cada uma gera as próprias chaves. Os sintomas são confusos: usuários deslogados a cada deploy, formulários que falham com 400 de antiforgery quando o POST cai numa réplica diferente do GET, mensagens de TempData que somem. A correção é compartilhar as chaves:

builder.Services.AddDataProtection()
    .SetApplicationName("livraria-site")
    .PersistKeysToStackExchangeRedis(
        ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!),
        "livraria:dataprotection:chaves");

O pacote é Microsoft.AspNetCore.DataProtection.StackExchangeRedis. Em nuvem, as chaves podem ir para Blob Storage ou S3, protegidas por uma chave num cofre (ProtectKeysWithAzureKeyVault e equivalentes). Esse é, com folga, o problema de deploy mais comum em aplicações ASP.NET Core com cookies.

Cabeçalhos encaminhados

Atrás do nginx, a aplicação recebe requisições HTTP simples, vindas do IP do nginx. Sem configuração, ela acha que todos os clientes têm o mesmo IP (e o rate limiting de "Produção" coloca todo mundo na mesma partição), gera links http:// em vez de https:// e o OpenID Connect monta uma URL de retorno errada, que o Keycloak recusa:

builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
    o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    o.KnownProxies.Clear();
    o.KnownProxies.Add(IPAddress.Parse(builder.Configuration["Borda:IpDoProxy"]!));
});
// primeiro middleware do pipeline
app.UseForwardedHeaders();

Os cabeçalhos só são aceitos vindos do endereço do nginx. Qualquer outro cliente que os envie é ignorado, pelo motivo discutido no rate limiting.

Segredos e ambiente

ASPNETCORE_ENVIRONMENT=Production (o padrão, se ausente), connection strings e segredos por variável de ambiente ou montados a partir do cofre, nunca na imagem. Uma imagem de contêiner é um artefato que se copia, publica e guarda em registros, e qualquer segredo dentro dela deve ser considerado vazado.

nginx na borda

O nginx fica na frente de tudo: encerra o TLS, serve como único ponto de entrada público, aplica limites de corpo e de conexão, e repassa as requisições ao site. A API só é alcançada pelo site, via YARP, e pelos clientes nativos, via um nome próprio.

upstream site { server site:8080; keepalive 32; }
upstream api  { server api:8080;  keepalive 32; }

map $http_upgrade $connection_upgrade { default upgrade; '' close; }

server {
    listen 443 ssl;
    http2 on;
    server_name loja.livraria.exemplo;

    ssl_certificate     /etc/letsencrypt/live/loja.livraria.exemplo/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/loja.livraria.exemplo/privkey.pem;

    client_max_body_size 8m;              # capas de até 5 MB, com folga

    location / {
        proxy_pass         http://site;
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        $connection_upgrade;
        proxy_read_timeout 100s;
    }
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.livraria.exemplo;
    # mesmos certificados e cabeçalhos; proxy_pass http://api;
}

server {
    listen 80;
    server_name loja.livraria.exemplo api.livraria.exemplo;
    return 301 https://$host$request_uri;
}

Os pontos que mais quebram na primeira tentativa:

  • Upgrade e Connection: sem eles, o WebSocket do SignalR não é repassado. O cliente cai para long polling e funciona, só que mais devagar, e o problema passa despercebido por semanas.
  • proxy_read_timeout: o padrão, 60 s, derruba conexões WebSocket ociosas. O keep-alive do SignalR, a cada 15 s, as mantém vivas, desde que esse valor seja maior que o intervalo.
  • client_max_body_size: o padrão do nginx é 1 MB. Sem o ajuste, o upload de capas falha com 413 antes de chegar ao ASP.NET Core, e nenhum log da aplicação mostra nada.
  • Certificados: o Let's Encrypt emite certificados gratuitos de 90 dias, e o certbot renova automaticamente. A renovação precisa ser monitorada, porque um certificado vencido derruba a loja inteira.

Para um servidor Linux sem contêineres, o equivalente é publicar o site num diretório e registrá-lo como serviço do systemd. Com o pacote Microsoft.Extensions.Hosting.Systemd e builder.Services.AddSystemd(), a aplicação avisa ao systemd quando está pronta e trata os sinais de parada. O nginx fica na frente exatamente como acima.

O sistema inteiro com Docker Compose

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: livraria
      POSTGRES_USER: livraria
      POSTGRES_PASSWORD_FILE: /run/secrets/senha_postgres
    volumes: [ "dados-postgres:/var/lib/postgresql/data" ]
    secrets: [ senha_postgres ]
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U livraria" ]
      interval: 5s

  migracoes:
    image: livraria/migracoes:1.4.0          # contém o efbundle
    depends_on: { postgres: { condition: service_healthy } }

  api:
    image: livraria/api:1.4.0
    environment:
      Livraria__Provedor: postgres
      ConnectionStrings__Postgres: "Host=postgres;Database=livraria;Username=livraria_app;Password=..."
      OTEL_EXPORTER_OTLP_ENDPOINT: http://coletor:4317
    depends_on:
      migracoes: { condition: service_completed_successfully }
      redis: { condition: service_healthy }
      rabbitmq: { condition: service_healthy }

  # redis, rabbitmq, keycloak, frete, worker, site, nginx, coletor...

volumes:
  dados-postgres:
secrets:
  senha_postgres: { file: ./segredos/senha_postgres.txt }

depends_on com condition expressa a ordem real: a API só sobe depois que as migrations terminaram com sucesso, e não apenas depois que o contêiner delas começou. Os dados ficam em volumes nomeados, que sobrevivem à recriação dos contêineres. Para um servidor único, Compose é suficiente e fácil de entender. Para vários servidores, com atualização gradual e reposição automática de réplicas, o passo seguinte é um orquestrador como Kubernetes ou os serviços gerenciados de contêiner das nuvens. Os conceitos (imagens, variáveis, checagens de saúde, volumes) são os mesmos.

Aspire: o sistema descrito em C#

Manter o docker-compose.yml, os launchSettings.json de cada projeto, as connection strings de desenvolvimento e o painel de telemetria sincronizados à mão é trabalhoso e sujeito a erros. O Aspire, criado pela Microsoft em 2023 como ".NET Aspire" e que em 2025 passou a se chamar apenas Aspire, com ciclo de versões próprio, descreve o sistema distribuído em código C#, num projeto chamado AppHost:

// Livraria.AppHost/AppHost.cs
var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres").WithDataVolume().AddDatabase("livraria");
var redis    = builder.AddRedis("redis");
var rabbit   = builder.AddRabbitMQ("rabbitmq").WithManagementPlugin();
var keycloak = builder.AddKeycloak("keycloak", 8081).WithDataVolume();

var frete = builder.AddProject<Projects.Livraria_Frete>("frete");

var api = builder.AddProject<Projects.Livraria_Api>("api")
    .WithReference(postgres).WaitFor(postgres)
    .WithReference(redis)
    .WithReference(rabbit).WaitFor(rabbit)
    .WithReference(frete)
    .WithReference(keycloak);

builder.AddProject<Projects.Livraria_Worker>("worker")
    .WithReference(rabbit).WaitFor(rabbit)
    .WithReference(redis);

builder.AddProject<Projects.Livraria_Site>("site")
    .WithReference(api)
    .WithReference(redis)
    .WithReference(keycloak)
    .WithExternalHttpEndpoints();

builder.Build().Run();

Um único dotnet run no AppHost sobe os contêineres de infraestrutura, inicia os quatro projetos .NET e abre o painel do Aspire, o mesmo que usamos avulso em "Produção". Nele estão os recursos, seus logs, os rastros distribuídos e as métricas, já ligados, porque o Aspire injeta as variáveis do OpenTelemetry em cada projeto.

WithReference faz duas coisas. Injeta no projeto as connection strings do recurso referenciado, na convenção ConnectionStrings__livraria (o nome do recurso: aqui, o banco criado por AddDatabase), e a configuração de service discovery. Dentro da API, o cliente gRPC do frete passa a usar um endereço lógico:

builder.Services.AddGrpcClient<Frete.FreteClient>(o => o.Address = new Uri("https+http://frete"));

https+http://frete significa "o serviço chamado frete, preferindo HTTPS". O endereço real é resolvido pela configuração injetada pelo AppHost em desenvolvimento, e pelas variáveis do ambiente de destino em produção. As portas aleatórias e os localhost:5090 escritos à mão desaparecem.

O segundo projeto padrão, ServiceDefaults, reúne numa única chamada o que cada serviço da Livraria configurou separadamente ao longo da Fase 6:

// em cada Program.cs
builder.AddServiceDefaults();   // OpenTelemetry, health checks, service discovery, resiliência padrão
// ...
app.MapDefaultEndpoints();      // /health e /alive

O ServiceDefaults é código seu, gerado no projeto, e pode ser editado. Não é uma caixa-preta. As integrações de cliente do Aspire, como builder.AddNpgsqlDbContext<LivrariaContext>("livraria") e builder.AddRedisClient("redis"), registram o cliente com resiliência, health checks e telemetria já ligados.

Para produção, o Aspire gera os artefatos de implantação a partir do mesmo modelo: um docker-compose.yml, manifestos de Kubernetes ou recursos de nuvem (Azure Container Apps e outros alvos, por meio de publicadores). O AppHost não roda em produção. Ele é a descrição a partir da qual o ambiente é produzido. O modelo do sistema passa a ser um só, do desenvolvimento ao deploy, versionado e revisado como qualquer código.

O Aspire não substitui entender o que está embaixo. Chaves de Data Protection, cabeçalhos encaminhados, bundles de migrations e o client_max_body_size do nginx continuam sendo responsabilidade de quem implanta. Ele apenas elimina a parte repetitiva e sujeita a esquecimento.

Da máquina ao servidor, automaticamente

O último elo é o pipeline de integração e entrega contínuas. Num fluxo típico com GitHub Actions:

  1. A cada pull request: dotnet build, dotnet test (os Testcontainers funcionam nos executores Linux do GitHub, que têm Docker), verificação de compatibilidade do OpenAPI com oasdiff, e análise de dependências vulneráveis com dotnet list package --vulnerable.
  2. A cada merge na branch principal: publicar as imagens com /t:PublishContainer e a versão tirada da tag Git, enviá-las ao registro, gerar o bundle de migrations.
  3. Deploy: aplicar as migrations, atualizar as imagens no servidor (docker compose pull && docker compose up -d, ou o orquestrador) e verificar /saude/pronta antes de declarar sucesso.
  4. Reversão: como as imagens têm tag de versão e as migrations são compatíveis com o código anterior, voltar é implantar a imagem anterior.

Lista de verificação de produção

  • [ ] ASPNETCORE_ENVIRONMENT=Production, sem páginas de exceção detalhadas.
  • [ ] Segredos fora da imagem e do repositório.
  • [ ] Chaves de Data Protection compartilhadas e persistentes.
  • [ ] UseForwardedHeaders com o proxy conhecido.
  • [ ] HTTPS com HSTS e redirecionamento. Certificados renovados e monitorados.
  • [ ] Migrations como passo separado, com conta de banco dedicada.
  • [ ] Imagens não root, chiseled com ICU.
  • [ ] Health checks de vida e prontidão ligados ao orquestrador.
  • [ ] OpenTelemetry exportando para um backend, com amostragem.
  • [ ] Rate limiting, CORS e limites de corpo configurados.
  • [ ] Backups dos bancos e testes de restauração: backup que nunca foi restaurado é hipótese.

A Livraria está no ar. Imagens enxutas e sem shell, geradas pelo próprio SDK; um nginx na borda que encerra TLS e repassa WebSockets e uploads; migrations aplicadas uma vez, com a conta certa; chaves e cabeçalhos configurados para que réplicas e reinícios não quebrem sessões; e um AppHost em C# que descreve o sistema inteiro, do primeiro dotnet run ao arquivo de deploy. Com isso a Fase 7 termina, e a Livraria tem banco, API e site completos. Um mesmo back-end, porém, costuma servir interfaces que não são páginas web: o caixa da loja física, um aplicativo no celular do cliente.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Depois do primeiro deploy com três réplicas do site, usuários relatam que "às vezes o formulário de aviso dá erro 400 e às vezes funciona", e que "todo deploy me desloga". Diagnostique as duas coisas com um único problema e dê a correção.

Ver resposta

✓ Resposta: As chaves de Data Protection estão no sistema de arquivos de cada contêiner. Cada réplica tem as suas. O GET que exibe o formulário gera o token antiforgery com as chaves da réplica A. Se o POST cair na réplica B, ela não consegue descriptografar o token e responde 400. Quando cai na mesma réplica, funciona, daí o "às vezes". A cada deploy, os contêineres são recriados, as chaves antigas somem, e nenhum cookie de sessão emitido antes pode mais ser lido, então todos são deslogados. A correção é AddDataProtection().SetApplicationName("livraria-site").PersistKeysToStackExchangeRedis(...) (ou outro armazenamento compartilhado e durável), com as chaves protegidas em repouso em produção. A afinidade de sessão no balanceador esconderia o primeiro sintoma, mas não o segundo, e não é a correção.

Exercício 2

Explique por que a publicação na imagem aspnet:10.0-noble-chiseled, sem o sufixo -extra, pode fazer a Livraria exibir preços como ¤39.90 e quebrar em datas. Como detectar isso antes de produção?

Ver resposta

✓ Resposta: A imagem chiseled sem -extra não inclui a biblioteca ICU nem os dados de fuso horário. O .NET detecta a ausência e, dependendo da configuração, entra no modo de globalização invariante ou falha ao iniciar. No modo invariante, toda cultura se comporta como a invariante: ToString("C") usa o símbolo genérico ¤ e ponto decimal, e UseRequestLocalization("pt-BR") não tem efeito. Sem os dados de fuso, TimeZoneInfo.FindSystemTimeZoneById("America/Sao_Paulo") lança TimeZoneNotFoundException. Para detectar antes: (1) rodar os testes de integração dentro da imagem de produção, e não só no SDK da máquina de build; (2) incluir um teste que afirma 39.90m.ToString("C", new CultureInfo("pt-BR")) == "R$ 39,90" (lembrando que o separador entre "R$" e o número é um espaço não separável) e que resolve o fuso de São Paulo; (3) declarar <InvariantGlobalization>false</InvariantGlobalization> no projeto, o que faz a aplicação falhar ao iniciar se o ICU estiver ausente, em vez de degradar em silêncio.

Exercício 3

Um desenvolvedor pôs db.Database.MigrateAsync() no Program.cs "para simplificar o deploy". Liste três problemas concretos com a Livraria em produção.

Ver resposta

✓ Resposta: (1) Concorrência: três réplicas iniciando juntas tentam aplicar as mesmas migrations ao mesmo tempo. Desde o EF Core 9 uma trava no banco as põe em fila, e a subida de todas fica esperando a mais lenta; antes disso, ou num provedor sem a trava, o resultado ia de erros de "tabela já existe" a esquemas parcialmente aplicados. (2) Privilégios: a aplicação precisa rodar com uma conta capaz de CREATE, ALTER e DROP. Uma injeção de SQL ou uma credencial vazada passa a poder destruir o esquema, e não só ler dados. (3) Deploy acoplado à inicialização: uma migration demorada (um índice numa tabela grande) atrasa a subida de todas as réplicas, os health checks falham, o orquestrador as reinicia no meio da migration, e o ciclo se repete. Há um quarto: a reversão para a imagem anterior não desfaz a migration, e o código antigo encontra um esquema novo que pode não entender. Um bundle como passo separado, com conta dedicada e migrations compatíveis com a versão anterior, resolve os quatro.

Exercício 4

O painel administrativo funciona em desenvolvimento, mas em produção as notificações de pedidos chegam com atraso de vários segundos, em rajadas. Os logs não mostram erros. Qual é a causa mais provável na configuração do nginx, e como confirmar?

Ver resposta

✓ Resposta: O nginx não está repassando o upgrade de WebSocket. Faltam proxy_http_version 1.1 e os cabeçalhos Upgrade e Connection na location que atende o hub. A negociação do SignalR tenta WebSocket, falha em silêncio, cai para SSE ou long polling, e as mensagens passam a chegar a cada ciclo de requisição, em rajadas. Nada gera erro, porque a queda para outro transporte é o comportamento desenhado. Para confirmar: nas ferramentas do navegador, na aba de rede, a conexão com o hub aparece como uma sequência de requisições poll em vez de uma única conexão WebSocket (código 101). No servidor, os logs de nível Debug do SignalR informam o transporte negociado. Uma métrica ou log do transporte usado por conexão tornaria o problema visível em produção.

Exercício 5

Descreva o que muda, e o que não muda, na forma como a API encontra o serviço de frete ao passar de endereços fixos na configuração para o service discovery do Aspire com https+http://frete.

Ver resposta

✓ Resposta: Muda: o código não contém mais host e porta. O nome lógico frete é resolvido a partir da configuração injetada: em desenvolvimento, o AppHost injeta o endpoint do projeto iniciado, com porta dinâmica; no Compose ou no Kubernetes gerados, as variáveis apontam para o nome do serviço na rede interna. O esquema https+http escolhe HTTPS quando disponível e cai para HTTP, útil em redes internas com TLS encerrado na borda. Trocar o ambiente não exige mexer em appsettings de cada projeto. Não muda: o transporte continua sendo gRPC sobre HTTP/2, com os mesmos prazos, a mesma resiliência e o mesmo problema de balanceamento entre réplicas discutido em "gRPC". O service discovery resolve o endereço, e não a distribuição de chamadas numa conexão HTTP/2 longa. A configuração continua sendo configuração: em produção alguém precisa garantir que o nome resolva para o destino certo, e um erro ali ainda derruba o frete.

Comentários

Mais em Linguagem C#

Memória no .NET: Pilha, Heap, Coletor de Lixo e IDisposable
Memória no .NET: Pilha, Heap, Coletor de Lixo e IDisposable

Memória no .NET: o que fica na pilha e no heap, por que alocar é barato e…

PostgreSQL: o Banco de Dados Servidor, com Npgsql, JSONB e Arrays
PostgreSQL: o Banco de Dados Servidor, com Npgsql, JSONB e Arrays

A Livraria muda para o PostgreSQL: Npgsql e NpgsqlDataSource, EF Core com…

Produção: CORS, rate limiting, versionamento e OpenTelemetry
Produção: CORS, rate limiting, versionamento e OpenTelemetry

Antes de ir para o público, a API da Livraria aprende a se defender e a ser…