//JorgenHoc
← Todos os artigos
.NET HostingPor Jorge CalderónAtualizado 17 min read

Hospedando uma API .NET no Fly.io — Passo a Passo

Faça o deploy da sua API .NET no Fly.io: Dockerfile, configuração do fly.toml, comandos flyctl, gerenciamento de segredos, addon Postgres, domínios personalizados, preços e prós e contras honestos.

#dotnet#cloud#devops#docker

O Fly.io silenciosamente se tornou uma das plataformas mais amigáveis para desenvolvedores que querem fazer deploy de cargas de trabalho em contêineres próximas aos seus usuários. Ele executa seus contêineres Docker em servidores bare-metal distribuídos em mais de 35 regiões, conectados por uma rede Anycast privada — o que significa que o tráfego entra pelo ponto de presença mais próximo, não por um único datacenter. Para APIs .NET, a história é simples: construa uma imagem Docker, configure um arquivo fly.toml e faça o push.

O Que é o Fly.io (e o Que Não É)

O Fly.io não é um PaaS no estilo Heroku. Ele se aproxima mais de uma plataforma de contêineres gerenciada que oferece:

  • Roteamento Anycast — um único IP roteia requisições para a instância saudável mais próxima no mundo todo.
  • Micro VMs (Firecracker) — cada contêiner roda dentro de uma VM leve, não em um namespace de contêiner compartilhado.
  • Placement global — faça deploy em uma região ou em várias com uma única flag.
  • Rede privada integrada — cada app recebe um nome DNS .internal em uma malha WireGuard.
  • Add-ons nativos de Postgres e Redis (gerenciados pelo Fly, não por terceiros).

O que ele não é: uma plataforma serverless, um cluster Kubernetes ou um host com buildpacks sem configuração. Você fornece o Dockerfile; o Fly.io o executa.

Rede Anycast e Regiões

Quando você executa fly deploy, o Fly.io coloca sua app na região que você especificar (padrão: iad — Northern Virginia). O tráfego para o seu IP público é roteado para a região mais próxima que tenha uma instância saudável.

# Listar todas as regiões disponíveis
fly platform regions
Código da RegiãoLocalização
iadAshburn, VA (EUA)
ordChicago, IL (EUA)
laxLos Angeles, CA (EUA)
lhrLondres, Reino Unido
fraFrankfurt, Alemanha
nrtTóquio, Japão
sydSydney, Austrália
gruSão Paulo, Brasil

Para uma API .NET que serve uma audiência global, você pode executar três instâncias — iad, lhr, nrt — e o Fly roteia cada usuário para a mais próxima automaticamente.


Configuração do Projeto

Comece com uma Web API .NET padrão gerada pela CLI (a configuração deste guia corresponde ao exemplo implantável em samples/dotnet-hosting, incluindo seu deploy/fly.toml):

dotnet new webapi -n MyApi --use-controllers
cd MyApi

A estrutura importa menos do que garantir que sua app escute na porta que o Fly.io espera. O Fly injeta PORT como variável de ambiente. Para serviços HTTP configurados no fly.toml, a porta interna padrão é 8080.

Configure o Kestrel para respeitar isso em Program.cs:

var builder = WebApplication.CreateBuilder(args);
 
// Fly.io define PORT em tempo de execução; fallback para 8080 no desenvolvimento local
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
 
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
 
var app = builder.Build();
 
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
 
app.UseAuthorization();
app.MapControllers();
app.Run();

Dockerfile Multi-Stage

Um Dockerfile de produção para .NET usa builds multi-stage para manter a imagem final pequena e executa como usuário não-root — ambas são boas práticas de segurança que o Fly.io também recomenda.

# syntax=docker/dockerfile:1
 
# ── Etapa de build ───────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
 
# Copiar o arquivo de projeto primeiro para que o cache do Docker sobreviva a mudanças de código-fonte
COPY MyApi.csproj ./
RUN dotnet restore
 
COPY . .
RUN dotnet publish -c Release -o /app/publish \
    --no-restore \
    /p:UseAppHost=false
 
# ── Etapa de runtime ─────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
 
# Criar um usuário não-root; as micro VMs do Fly.io isolam no nível da VM,
# mas executar como não-root é defesa em profundidade
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
 
COPY --from=build /app/publish .
 
# Mudar para usuário não-root antes do CMD final
USER appuser
 
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE 8080
 
ENTRYPOINT ["dotnet", "MyApi.dll"]
💡

A imagem runtime mcr.microsoft.com/dotnet/aspnet:10.0 pesa ~220 MB. Se o tamanho importa, mude para mcr.microsoft.com/dotnet/aspnet:10.0-alpine (~100 MB), mas note que Alpine usa musl libc, o que pode afetar alguns cenários de interoperabilidade nativa.

Verifique se a imagem é construída localmente antes de tocar no Fly:

docker build -t myapi:local .
docker run --rm -p 8080:8080 -e ASPNETCORE_ENVIRONMENT=Development myapi:local

Instalando o flyctl

flyctl é a única ferramenta CLI para tudo no Fly.io.

# macOS / Linux
curl -L https://fly.io/install.sh | sh
 
# Windows (PowerShell)
iwr https://fly.io/install.ps1 -useb | iex
 
# Homebrew
brew install flyctl

Autentique-se:

fly auth login

Isso abre um navegador. Após o login, o flyctl armazena um token em ~/.fly/config.yml.


fly launch — Primeiro Deploy

fly launch é um assistente interativo que detecta seu Dockerfile, cria a app no Fly.io, escreve o fly.toml e opcionalmente faz o deploy imediatamente.

fly launch

Você será solicitado a fornecer:

  1. Nome da app — deve ser globalmente único entre todos os clientes do Fly.io (ex.: myapi-prod).
  2. Região primária — escolha a região mais próxima dos seus usuários.
  3. Banco de dados Postgres — recuse aqui; vamos configurá-lo separadamente abaixo.
  4. Fazer deploy agora? — sim.

Após o assistente terminar, examine o fly.toml gerado.


Configuração do fly.toml

fly.toml é a fonte da verdade para a configuração de deploy da sua app. Aqui está uma versão anotada pronta para produção:

# O nome da app deve corresponder ao que você criou com fly launch
app = "myapi-prod"
 
# Fly.io constrói a partir do Dockerfile no diretório atual por padrão
[build]
  dockerfile = "Dockerfile"
 
# Região primária — onde a primeira instância vive
primary_region = "iad"
 
# Variáveis de ambiente que NÃO são segredos
# Segredos (strings de conexão, API keys) vão em fly secrets, não aqui
[env]
  ASPNETCORE_ENVIRONMENT = "Production"
  DOTNET_SYSTEM_GLOBALIZATION_INVARIANT = "false"
 
# Configuração do serviço HTTP; o Fly.io termina TLS na borda e encaminha HTTP
[http_service]
  internal_port = 8080          # Deve corresponder ao EXPOSE no seu Dockerfile
  force_https = true            # Redirecionar HTTP → HTTPS automaticamente
  auto_stop_machines = true     # Parar máquinas ociosas para economizar custos
  auto_start_machines = true    # Iniciar uma máquina quando uma requisição chega
  min_machines_running = 0      # 0 = scale-to-zero completo; 1 = sempre ativo
 
  [http_service.concurrency]
    type = "requests"
    hard_limit = 250
    soft_limit = 200
 
[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 256
⚠️

auto_stop_machines = true com min_machines_running = 0 habilita scale-to-zero. Sua primeira requisição após um período de inatividade experimentará um cold start (veja a seção Cold Starts abaixo). Defina min_machines_running = 1 para APIs de produção onde a latência importa.

Adicionando um Endpoint de Health Check

O Fly.io usa a estrofe [[http_service.checks]] para determinar a saúde da máquina. Adicione um endpoint dedicado na sua API:

// Health check mínimo — sem dependências, apenas confirma que o processo está vivo
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }))
   .WithName("HealthCheck")
   .AllowAnonymous();

Depois referencie no fly.toml:

[http_service]
  internal_port = 8080
  force_https = true
 
  [[http_service.checks]]
    grace_period = "10s"   # Tempo de espera antes do primeiro check após o início
    interval = "15s"
    method = "GET"
    path = "/health"
    timeout = "5s"

Comandos Principais do flyctl

ComandoO Que Faz
fly launchAssistente interativo de primeiro deploy
fly deployConstrói a imagem e faz deploy; usa o daemon Docker local por padrão
fly deploy --remote-onlyConstrói no builder remoto do Fly (não precisa de Docker local)
fly statusMostra as máquinas em execução e sua saúde
fly logsAcompanha logs ao vivo de todas as máquinas
fly logs -i <machine-id>Logs de uma máquina específica
fly ssh consoleAbre um shell dentro de uma máquina em execução
fly scale count 3Escala para 3 máquinas na região primária
fly scale count 1 --region lhrGarante 1 máquina em Londres
fly releasesLista todos os deploys com tags de imagem
fly rollbackReverte para o release anterior
fly apps destroy myapi-prodExclui permanentemente a app

Fazendo Deploy Após Mudanças no Código

# O ciclo padrão de deploy
fly deploy
 
# Acompanhar o progresso do deploy
fly status --watch
 
# Acompanhar logs após o deploy
fly logs

O Fly.io realiza um deploy rolling por padrão: inicia novas máquinas, aguarda os health checks passarem, depois remove as máquinas antigas. Zero-downtime por padrão.


Gerenciamento de Segredos

Nunca coloque credenciais no fly.toml ou em variáveis de ambiente que acabem no controle de código-fonte. Use fly secrets:

# Definir um segredo (criptografado em repouso, injetado como variável de ambiente em runtime)
fly secrets set DATABASE_URL="postgresql://user:pass@hostname/db"
 
# Definir vários de uma vez
fly secrets set \
  JWT_SECRET="seu-segredo-jwt-aqui" \
  SENDGRID_API_KEY="SG.xxxx"
 
# Listar nomes de segredos (valores nunca são mostrados)
fly secrets list
 
# Remover um segredo
fly secrets unset SENDGRID_API_KEY

No seu código .NET, os segredos chegam como variáveis de ambiente padrão:

// appsettings.json tem a chave; o valor é sobrescrito pela variável de ambiente em runtime
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(
        // Variável de ambiente DATABASE_URL definida via fly secrets
        builder.Configuration.GetConnectionString("Default")
        ?? Environment.GetEnvironmentVariable("DATABASE_URL")
        ?? throw new InvalidOperationException("DATABASE_URL não está configurado")
    )
);

Adicionando Postgres

O Fly.io oferece clusters Postgres gerenciados que rodam como apps Fly separadas na sua conta. Eles não são serverless — são VMs persistentes com volumes anexados.

Criar o Cluster Postgres

# Cria um cluster Postgres HA de 2 nós chamado "myapi-db" na região iad
fly postgres create \
  --name myapi-db \
  --region iad \
  --vm-size shared-cpu-1x \
  --volume-size 10
 
# A saída inclui a string de conexão — salve-a, não será mostrada novamente
# postgres://myapi_db:SENHA@myapi-db.flycast:5432/myapi_db

Anexar à Sua App

# Anexa o cluster Postgres à sua app e define DATABASE_URL automaticamente
fly postgres attach --app myapi-prod myapi-db

attach cria um usuário de banco de dados com escopo para sua app, define o segredo DATABASE_URL e configura a rede privada para que sua app alcance o Postgres através da malha WireGuard (nunca pela internet pública).

Verifique se o segredo foi definido:

fly secrets list
# NOME          DIGEST    CRIADO EM
# DATABASE_URL  abc123    2025-02-21T10:00:00Z

String de Conexão para EF Core

O formato DATABASE_URL do Fly é uma URI libpq. O Npgsql a aceita diretamente:

// Parsear DATABASE_URL do formato do Fly.io: postgres://user:pass@host:port/db
var rawUrl = builder.Configuration["DATABASE_URL"]
    ?? Environment.GetEnvironmentVariable("DATABASE_URL");
 
if (!string.IsNullOrEmpty(rawUrl))
{
    var uri = new Uri(rawUrl);
    var userInfo = uri.UserInfo.Split(':');
    var connectionString = new NpgsqlConnectionStringBuilder
    {
        Host = uri.Host,
        Port = uri.Port == -1 ? 5432 : uri.Port,
        Database = uri.AbsolutePath.TrimStart('/'),
        Username = userInfo[0],
        Password = userInfo.Length > 1 ? userInfo[1] : null,
        SslMode = SslMode.Prefer,    // A rede interna do Fly já é criptografada via WireGuard
    }.ToString();
 
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseNpgsql(connectionString));
}

Executando Migrações do EF Core no Deploy

A abordagem mais limpa é um release_command no fly.toml — o Fly o executa antes de rotear tráfego para novas máquinas:

[deploy]
  release_command = "dotnet MyApi.dll migrate"

Adicione um handler de comando personalizado em Program.cs:

// Verificar args da CLI antes de construir a app completa
if (args.Contains("migrate"))
{
    // Construir um host mínimo apenas para migração
    var host = Host.CreateDefaultBuilder(args)
        .ConfigureServices((ctx, services) =>
        {
            services.AddDbContext<AppDbContext>(options =>
                options.UseNpgsql(ctx.Configuration["DATABASE_URL"]));
        })
        .Build();
 
    using var scope = host.Services.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync();
    Console.WriteLine("Migrações aplicadas com sucesso.");
    return;
}
 
// Inicialização normal da app continua abaixo...

Escalamento Horizontal e Multi-Região

Escalar Dentro de uma Região

# Executar 3 máquinas na região primária (iad)
fly scale count 3
 
# Verificar
fly status

O balanceador de carga do Fly.io distribui requisições entre todas as máquinas saudáveis usando least-connections.

Deploy Multi-Região

# Adicionar máquinas em Frankfurt e Tóquio
fly scale count 1 --region fra
fly scale count 1 --region nrt
 
# Verificar a distribuição
fly status

Seu fly.toml pode fixar certas regiões:

# Manter pelo menos uma máquina em cada região o tempo todo
[[regions]]
  code = "iad"
  count = 2
 
[[regions]]
  code = "lhr"
  count = 1
💡

Para APIs .NET com EF Core e um único cluster Postgres, tenha cuidado com writes multi-região. O Postgres gerenciado do Fly não replica automaticamente writes para regiões réplica — todos os writes vão para o primário. Use headers fly-replay ou roteie endpoints com muitos writes para a região primária.


Domínios Personalizados e TLS

O Fly.io provisiona certificados TLS automaticamente via Let's Encrypt.

# Adicionar um domínio personalizado (você deve ser o proprietário)
fly certs add api.seudominio.com
 
# Verificar status do certificado
fly certs show api.seudominio.com

A saída inclui dois registros DNS para adicionar no seu registrador:

Tipo  Host                    Valor
A     api.seudominio.com      66.241.124.x
AAAA  api.seudominio.com      2a09:8280:1::...

Ou use um CNAME apontando para myapi-prod.fly.dev. Os certificados são emitidos em minutos após a propagação DNS.


Comportamento do Cold Start

Com auto_stop_machines = true e min_machines_running = 0, o Fly.io para sua máquina após ~5 minutos sem tráfego. A próxima requisição dispara um cold start.

Linha do tempo de cold start para uma API .NET:

FaseDuração Típica
Boot da VM Fly.io (Firecracker)~300 ms
Pull da imagem Docker (primeiro deploy)~0 ms (imagem local ao host)
Inicialização do runtime .NET~200–600 ms
Pipeline de middleware ASP.NET~50–100 ms
Total~550–1000 ms

Isso é rápido comparado aos cold starts do .NET no AWS Lambda, mas ainda perceptível para APIs interativas.

Como Evitar Cold Starts

Opção 1: Definir min_machines_running = 1

[http_service]
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1   # Manter sempre uma máquina aquecida

Isso custa ~$1,94/mês para uma máquina shared-cpu-1x — essencialmente gratuito para produção.

Opção 2: Startup mais rápido com Native AOT do .NET

<!-- MyApi.csproj — habilita compilação Native AOT -->
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

Apps .NET compiladas com AOT iniciam em ~50–100 ms no total. A contrapartida: tempos de build mais longos, sem reflexão em runtime e alguns pacotes NuGet não são compatíveis com AOT.

Opção 3: Requisição de aquecimento via health check

Configure um monitor externo de uptime (ex.: BetterUptime, tier gratuito do UptimeRobot) para fazer ping em /health a cada 4 minutos. Isso mantém a máquina ativa sem pagar por min_machines_running.


Preços

Os preços do Fly.io (em 2026) são baseados em uso: você paga por segundo de execução de máquina, não pela capacidade provisionada. Máquinas paradas não acumulam cobranças de computação (você ainda paga pelos volumes e pelos endereços IPv4 reservados).

Computação (CPU Compartilhada)

Tamanho da MáquinavCPURAMPreço/Mês (completo)
shared-cpu-1x1 compartilhada256 MB~$1,94
shared-cpu-1x1 compartilhada512 MB~$3,13
shared-cpu-2x2 compartilhadas512 MB~$5,70
shared-cpu-4x4 compartilhadas1 GB~$10,70

Computação (CPU Dedicada)

Tamanho da MáquinavCPURAMPreço/Mês
performance-1x1 dedicada2 GB~$7,69
performance-2x2 dedicadas4 GB~$15,38
performance-4x4 dedicadas8 GB~$30,77

Postgres

PlanoRAMArmazenamentoPreço/Mês
shared-cpu-1x256 MB1 GB~$1,94
shared-cpu-1x256 MB10 GB~$3,44
performance-1x2 GB50 GB~$26,69

Teste Gratuito, Não um Tier Gratuito

O Fly.io não oferece mais uma allowance gratuita permanente. Organizações novas recebem um crédito de teste único, e o cadastro exige um cartão de crédito — se você não pode ou não quer adicionar um, o Fly.io fica fora de cogitação, e o tier gratuito do Render é a alternativa mais próxima. Contas criadas antes da mudança de preços podem manter allowances herdadas.

Dito isso, o scale-to-zero mantém os custos reais mínimos: uma máquina shared-cpu-1x com auto_stop_machines = true que só roda quando chega tráfego cobra centavos por mês para uma API de baixo tráfego.

💡

Verifique o acumulado do mês atual no dashboard do Fly.io em Billing, e configure um alerta de gastos para evitar surpresas.


Fluxo Completo de Deploy

Aqui está a sequência completa do zero à produção:

# 1. Instalar flyctl e autenticar
curl -L https://fly.io/install.sh | sh
fly auth login
 
# 2. Criar a app (gera fly.toml)
fly launch --name myapi-prod --region iad --no-deploy
 
# 3. Definir segredos da aplicação
fly secrets set \
  JWT_SECRET="$(openssl rand -base64 32)" \
  ENVIRONMENT="Production"
 
# 4. Criar e anexar Postgres
fly postgres create --name myapi-db --region iad --vm-size shared-cpu-1x
fly postgres attach --app myapi-prod myapi-db
 
# 5. Fazer deploy
fly deploy
 
# 6. Verificar
fly status
fly logs
 
# 7. Abrir no navegador
fly open

Prós e Contras para APIs .NET

Prós

AspectoDetalhe
Anycast GlobalRoteia usuários para a região mais próxima com um único IP — sem configuração de CDN
Firecracker VMsMelhor isolamento que contêineres compartilhados; latência previsível
Rede privadaO tráfego app-para-Postgres permanece na malha WireGuard, nunca é público
Suporte Docker .NETAs imagens oficiais da Microsoft funcionam perfeitamente; sem atrito com buildpacks
Rolling deploysZero-downtime de fábrica, sem YAML do Kubernetes
Acesso SSHfly ssh console dá um shell real para debugging
PreçoShared-cpu-1x a $1,94/mês é difícil de superar para APIs pequenas
Scale-to-zeroMáquinas paradas não cobram nada — apps ociosas custam centavos por mês

Contras

AspectoDetalhe
Cold startsScale-to-zero significa ~1s de cold start; não ideal para APIs com SLA rigoroso
Sem SQL Server gerenciadoO Fly oferece Postgres e Redis; se precisar de SQL Server, você o executa sozinho
Fly Postgres é semi-DIYÉ Postgres em uma VM, não um serviço totalmente gerenciado como RDS — você gerencia extensões e backups manualmente
Gerenciamento de volumesVolumes persistentes são bloqueados por região; apps com estado multi-região são complexas
Sem APM integradoSem equivalente ao Application Insights; integre OpenTelemetry + um provedor externo
Ecossistema menorMenos tutoriais, menos cobertura no StackOverflow do que AWS/Azure
Configuração do WireGuardConectar da sua máquina local a serviços internos do Fly requer fly proxy ou um cliente WireGuard

Resumo

CenárioRecomendação
Projeto pessoal / side projectO scale-to-zero mantém o custo perto de zero — mas o cadastro exige cartão de crédito
API de produção pequena (<100 req/s)shared-cpu-1x, min_machines_running = 1, Fly Postgres
API global (baixa latência mundial)Deploy multi-região com 3–5 máquinas
API de alto tráfego (>500 req/s)Máquinas performance-1x + cluster Postgres HA
Requisitos empresariais / complianceAWS/Azure (mais ferramentas de auditoria, SLAs, SQL Server nativo)
Equipe já no KubernetesConsidere o Fly.io para serviços menores; apps maiores podem superar seus limites

O Fly.io atinge um ponto ótimo para desenvolvedores .NET que querem deploys nativos em Docker, presença global e preços transparentes sem a complexidade do AWS ECS/EKS ou a sobrecarga de configuração do Azure App Service. A CLI flyctl é genuinamente agradável de usar, a rede privada baseada em WireGuard é uma vantagem de segurança sobre o peering VPC tradicional, e o isolamento da Firecracker VM significa que seu contêiner se comporta como uma máquina real. O principal problema para .NET é a oferta de banco de dados gerenciado apenas com Postgres — se sua app requer SQL Server, você o estará executando em uma VM simples.

Leituras adicionais

Sobre o autor

Jorge Calderón

Engenheiro de software com mais de uma década construindo e operando aplicações .NET em produção — camadas de dados com EF Core, serviços intensivos em async e implantações em Azure e contêineres. Todos os benchmarks e projetos de exemplo destes guias estão publicados em um repositório público no GitHub para que você possa reproduzi-los.

Perfil no GitHubLinkedIn ↗Benchmarks e código de exemplo

Artigos relacionados