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:
UpgradeeConnection: 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
certbotrenova 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:
- 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 comoasdiff, e análise de dependências vulneráveis comdotnet list package --vulnerable. - A cada merge na branch principal: publicar as imagens com
/t:PublishContainere a versão tirada da tag Git, enviá-las ao registro, gerar o bundle de migrations. - Deploy: aplicar as migrations, atualizar as imagens no servidor (
docker compose pull && docker compose up -d, ou o orquestrador) e verificar/saude/prontaantes de declarar sucesso. - 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.
- [ ]
UseForwardedHeaderscom 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
- Microsoft Learn — Containerize a .NET app with dotnet publish: https://learn.microsoft.com/dotnet/core/containers/sdk-publish
- Microsoft Learn — Host ASP.NET Core on Linux with Nginx: https://learn.microsoft.com/aspnet/core/host-and-deploy/linux-nginx
- Microsoft Learn — Configure ASP.NET Core Data Protection: https://learn.microsoft.com/aspnet/core/security/data-protection/configuration/overview
- Microsoft Learn — Applying Migrations (migration bundles e migration locking): https://learn.microsoft.com/ef/core/managing-schemas/migrations/applying
- Aspire — documentação oficial: https://aspire.dev/
- nginx — WebSocket proxying: https://nginx.org/en/docs/http/websocket.html
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.