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
.internalem 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ão | Localização |
|---|---|
| iad | Ashburn, VA (EUA) |
| ord | Chicago, IL (EUA) |
| lax | Los Angeles, CA (EUA) |
| lhr | Londres, Reino Unido |
| fra | Frankfurt, Alemanha |
| nrt | Tóquio, Japão |
| syd | Sydney, Austrália |
| gru | Sã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 MyApiA 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:localInstalando 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 flyctlAutentique-se:
fly auth loginIsso 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 launchVocê será solicitado a fornecer:
- Nome da app — deve ser globalmente único entre todos os clientes do Fly.io (ex.:
myapi-prod). - Região primária — escolha a região mais próxima dos seus usuários.
- Banco de dados Postgres — recuse aqui; vamos configurá-lo separadamente abaixo.
- 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 = 256auto_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
| Comando | O Que Faz |
|---|---|
fly launch | Assistente interativo de primeiro deploy |
fly deploy | Constrói a imagem e faz deploy; usa o daemon Docker local por padrão |
fly deploy --remote-only | Constrói no builder remoto do Fly (não precisa de Docker local) |
fly status | Mostra as máquinas em execução e sua saúde |
fly logs | Acompanha logs ao vivo de todas as máquinas |
fly logs -i <machine-id> | Logs de uma máquina específica |
fly ssh console | Abre um shell dentro de uma máquina em execução |
fly scale count 3 | Escala para 3 máquinas na região primária |
fly scale count 1 --region lhr | Garante 1 máquina em Londres |
fly releases | Lista todos os deploys com tags de imagem |
fly rollback | Reverte para o release anterior |
fly apps destroy myapi-prod | Exclui 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 logsO 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_KEYNo 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_dbAnexar à Sua App
# Anexa o cluster Postgres à sua app e define DATABASE_URL automaticamente
fly postgres attach --app myapi-prod myapi-dbattach 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:00ZString 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 statusO 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 statusSeu 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 = 1Para 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.comA 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:
| Fase | Duraçã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 aquecidaIsso 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áquina | vCPU | RAM | Preço/Mês (completo) |
|---|---|---|---|
| shared-cpu-1x | 1 compartilhada | 256 MB | ~$1,94 |
| shared-cpu-1x | 1 compartilhada | 512 MB | ~$3,13 |
| shared-cpu-2x | 2 compartilhadas | 512 MB | ~$5,70 |
| shared-cpu-4x | 4 compartilhadas | 1 GB | ~$10,70 |
Computação (CPU Dedicada)
| Tamanho da Máquina | vCPU | RAM | Preço/Mês |
|---|---|---|---|
| performance-1x | 1 dedicada | 2 GB | ~$7,69 |
| performance-2x | 2 dedicadas | 4 GB | ~$15,38 |
| performance-4x | 4 dedicadas | 8 GB | ~$30,77 |
Postgres
| Plano | RAM | Armazenamento | Preço/Mês |
|---|---|---|---|
| shared-cpu-1x | 256 MB | 1 GB | ~$1,94 |
| shared-cpu-1x | 256 MB | 10 GB | ~$3,44 |
| performance-1x | 2 GB | 50 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 openPrós e Contras para APIs .NET
Prós
| Aspecto | Detalhe |
|---|---|
| Anycast Global | Roteia usuários para a região mais próxima com um único IP — sem configuração de CDN |
| Firecracker VMs | Melhor isolamento que contêineres compartilhados; latência previsível |
| Rede privada | O tráfego app-para-Postgres permanece na malha WireGuard, nunca é público |
| Suporte Docker .NET | As imagens oficiais da Microsoft funcionam perfeitamente; sem atrito com buildpacks |
| Rolling deploys | Zero-downtime de fábrica, sem YAML do Kubernetes |
| Acesso SSH | fly ssh console dá um shell real para debugging |
| Preço | Shared-cpu-1x a $1,94/mês é difícil de superar para APIs pequenas |
| Scale-to-zero | Máquinas paradas não cobram nada — apps ociosas custam centavos por mês |
Contras
| Aspecto | Detalhe |
|---|---|
| Cold starts | Scale-to-zero significa ~1s de cold start; não ideal para APIs com SLA rigoroso |
| Sem SQL Server gerenciado | O 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 volumes | Volumes persistentes são bloqueados por região; apps com estado multi-região são complexas |
| Sem APM integrado | Sem equivalente ao Application Insights; integre OpenTelemetry + um provedor externo |
| Ecossistema menor | Menos tutoriais, menos cobertura no StackOverflow do que AWS/Azure |
| Configuração do WireGuard | Conectar da sua máquina local a serviços internos do Fly requer fly proxy ou um cliente WireGuard |
Resumo
| Cenário | Recomendação |
|---|---|
| Projeto pessoal / side project | O 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 / compliance | AWS/Azure (mais ferramentas de auditoria, SLAs, SQL Server nativo) |
| Equipe já no Kubernetes | Considere 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.