Fly.io se ha convertido silenciosamente en una de las plataformas más amigables para desarrolladores que quieren desplegar cargas de trabajo en contenedores cerca de sus usuarios. Ejecuta tus contenedores Docker en servidores bare-metal distribuidos en más de 35 regiones, conectados por una red Anycast privada — lo que significa que el tráfico entra por el punto de presencia más cercano, no por un único centro de datos. Para APIs .NET, la historia es simple: construye una imagen Docker, configura un archivo fly.toml y hace push.
Qué es Fly.io (y qué no es)
Fly.io no es un PaaS al estilo Heroku. Se acerca más a una plataforma de contenedores gestionada que te ofrece:
- Enrutamiento Anycast — una única IP enruta las solicitudes a la instancia sana más cercana a nivel mundial.
- Micro VMs (Firecracker) — cada contenedor corre dentro de una VM ligera, no en un namespace de contenedores compartido.
- Placement global — despliega en una región o en muchas con un solo flag.
- Red privada integrada — cada app obtiene un nombre DNS
.internalen una malla WireGuard. - Add-ons nativos de Postgres y Redis (gestionados por Fly, no por terceros).
Lo que no es: una plataforma serverless, un clúster de Kubernetes ni un host con buildpacks sin configuración. Tú provees el Dockerfile; Fly.io lo ejecuta.
Red Anycast y Regiones
Cuando ejecutas fly deploy, Fly.io coloca tu app en la región que especifiques (por defecto: iad — Northern Virginia). El tráfico a tu IP pública se enruta a la región más cercana que tenga una instancia sana.
# Listar todas las regiones disponibles
fly platform regions| Código de Región | Ubicación |
|---|---|
| iad | Ashburn, VA (EE.UU.) |
| ord | Chicago, IL (EE.UU.) |
| lax | Los Ángeles, CA (EE.UU.) |
| lhr | Londres, Reino Unido |
| fra | Frankfurt, Alemania |
| nrt | Tokio, Japón |
| syd | Sídney, Australia |
| gru | São Paulo, Brasil |
Para una API .NET que sirve a una audiencia global, puedes ejecutar tres instancias — iad, lhr, nrt — y Fly enruta a cada usuario a la más cercana automáticamente.
Configuración del Proyecto
Comienza con una Web API .NET estándar generada por la CLI (la configuración de esta guía coincide con el ejemplo desplegable en samples/dotnet-hosting, incluido su deploy/fly.toml):
dotnet new webapi -n MyApi --use-controllers
cd MyApiLa estructura importa menos que asegurarte de que tu app escuche en el puerto que Fly.io espera. Fly inyecta PORT como variable de entorno. Para servicios HTTP configurados en fly.toml, el puerto interno por defecto es 8080.
Configura Kestrel para respetar eso en Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Fly.io establece PORT en tiempo de ejecución; fallback a 8080 para desarrollo 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
Un Dockerfile de producción para .NET usa builds multi-stage para mantener la imagen final pequeña y ejecuta como usuario no-root — ambas son buenas prácticas de seguridad que Fly.io también recomienda.
# syntax=docker/dockerfile:1
# ── Etapa de build ───────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copiar el archivo de proyecto primero para que la caché de Docker sobreviva cambios de código fuente
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
# Crear un usuario no-root; las micro VMs de Fly.io aíslan a nivel VM,
# pero ejecutar como no-root es defensa en profundidad
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
COPY --from=build /app/publish .
# Cambiar a usuario no-root antes del CMD final
USER appuser
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApi.dll"]La imagen runtime mcr.microsoft.com/dotnet/aspnet:10.0 pesa ~220 MB. Si el tamaño importa, cambia a mcr.microsoft.com/dotnet/aspnet:10.0-alpine (~100 MB), pero ten en cuenta que Alpine usa musl libc, lo que puede afectar algunos escenarios de interoperabilidad nativa.
Verifica que la imagen se construya localmente antes de tocar Fly:
docker build -t myapi:local .
docker run --rm -p 8080:8080 -e ASPNETCORE_ENVIRONMENT=Development myapi:localInstalación de flyctl
flyctl es la única herramienta CLI para todo en 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 flyctlAutentícate:
fly auth loginEsto abre un navegador. Después del login, flyctl guarda un token en ~/.fly/config.yml.
fly launch — Primer Despliegue
fly launch es un asistente interactivo que detecta tu Dockerfile, crea la app en Fly.io, escribe fly.toml y opcionalmente despliega de inmediato.
fly launchSe te pedirá:
- Nombre de la app — debe ser globalmente único entre todos los clientes de Fly.io (ej.:
myapi-prod). - Región primaria — elige la región más cercana a tus usuarios.
- Base de datos Postgres — recházalo aquí; lo configuraremos por separado más abajo.
- ¿Desplegar ahora? — sí.
Después de que termine el asistente, examina el fly.toml generado.
Configuración de fly.toml
fly.toml es la fuente de verdad para la configuración de despliegue de tu app. Aquí tienes una versión anotada lista para producción:
# El nombre de la app debe coincidir con lo que creaste con fly launch
app = "myapi-prod"
# Fly.io construye desde el Dockerfile en el directorio actual por defecto
[build]
dockerfile = "Dockerfile"
# Región primaria — donde vive la primera instancia
primary_region = "iad"
# Variables de entorno que NO son secretos
# Los secretos (cadenas de conexión, API keys) van en fly secrets, no aquí
[env]
ASPNETCORE_ENVIRONMENT = "Production"
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT = "false"
# Configuración del servicio HTTP; Fly.io termina TLS en el edge y reenvía HTTP
[http_service]
internal_port = 8080 # Debe coincidir con EXPOSE en tu Dockerfile
force_https = true # Redirigir HTTP → HTTPS automáticamente
auto_stop_machines = true # Detener máquinas inactivas para ahorrar costos
auto_start_machines = true # Iniciar una máquina cuando llega una solicitud
min_machines_running = 0 # 0 = scale-to-zero completo; 1 = siempre activo
[http_service.concurrency]
type = "requests"
hard_limit = 250
soft_limit = 200
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256auto_stop_machines = true con min_machines_running = 0 habilita scale-to-zero. Tu primera solicitud después de un período de inactividad experimentará un arranque en frío (ver la sección Cold Starts más abajo). Establece min_machines_running = 1 para APIs de producción donde la latencia importa.
Agregar un Endpoint de Health Check
Fly.io usa la estrofa [[http_service.checks]] para determinar la salud de la máquina. Agrega un endpoint dedicado en tu API:
// Health check mínimo — sin dependencias, solo confirma que el proceso está vivo
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }))
.WithName("HealthCheck")
.AllowAnonymous();Luego referencíalo en fly.toml:
[http_service]
internal_port = 8080
force_https = true
[[http_service.checks]]
grace_period = "10s" # Tiempo de espera antes del primer check después del inicio
interval = "15s"
method = "GET"
path = "/health"
timeout = "5s"Comandos Clave de flyctl
| Comando | Qué Hace |
|---|---|
fly launch | Asistente interactivo de primer despliegue |
fly deploy | Construye imagen y despliega; usa el daemon Docker local por defecto |
fly deploy --remote-only | Construye en el builder remoto de Fly (no se necesita Docker local) |
fly status | Muestra las máquinas en ejecución y su salud |
fly logs | Sigue los logs en vivo de todas las máquinas |
fly logs -i <machine-id> | Logs de una máquina específica |
fly ssh console | Abre una shell dentro de una máquina en ejecución |
fly scale count 3 | Escala a 3 máquinas en la región primaria |
fly scale count 1 --region lhr | Asegura 1 máquina en Londres |
fly releases | Lista todos los despliegues con tags de imagen |
fly rollback | Revierte al release anterior |
fly apps destroy myapi-prod | Elimina permanentemente la app |
Desplegando Después de Cambios en el Código
# El ciclo estándar de despliegue
fly deploy
# Observar el progreso del despliegue
fly status --watch
# Seguir logs después del despliegue
fly logsFly.io realiza un despliegue rolling por defecto: inicia nuevas máquinas, espera a que los health checks pasen, luego elimina las máquinas antiguas. Zero-downtime por defecto.
Gestión de Secretos
Nunca pongas credenciales en fly.toml o en variables de entorno que terminen en control de código fuente. Usa fly secrets:
# Establecer un secreto (encriptado en reposo, inyectado como variable de entorno en runtime)
fly secrets set DATABASE_URL="postgresql://user:pass@hostname/db"
# Establecer varios a la vez
fly secrets set \
JWT_SECRET="tu-secreto-jwt-aqui" \
SENDGRID_API_KEY="SG.xxxx"
# Listar nombres de secretos (los valores nunca se muestran)
fly secrets list
# Eliminar un secreto
fly secrets unset SENDGRID_API_KEYEn tu código .NET, los secretos llegan como variables de entorno estándar:
// appsettings.json tiene la clave; el valor es sobreescrito por la variable de entorno en runtime
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(
// Variable de entorno DATABASE_URL establecida via fly secrets
builder.Configuration.GetConnectionString("Default")
?? Environment.GetEnvironmentVariable("DATABASE_URL")
?? throw new InvalidOperationException("DATABASE_URL no está configurado")
)
);Agregando Postgres
Fly.io ofrece clústeres Postgres gestionados que se ejecutan como apps Fly separadas en tu cuenta. No son serverless — son VMs persistentes con volúmenes adjuntos.
Crear el Clúster Postgres
# Crea un clúster Postgres HA de 2 nodos llamado "myapi-db" en la región iad
fly postgres create \
--name myapi-db \
--region iad \
--vm-size shared-cpu-1x \
--volume-size 10
# La salida incluye la cadena de conexión — guárdala, no se mostrará de nuevo
# postgres://myapi_db:CONTRASEÑA@myapi-db.flycast:5432/myapi_dbAdjuntar a tu App
# Adjunta el clúster Postgres a tu app y establece DATABASE_URL automáticamente
fly postgres attach --app myapi-prod myapi-dbattach crea un usuario de base de datos con scope a tu app, establece el secreto DATABASE_URL y configura la red privada para que tu app alcance Postgres a través de la malla WireGuard (nunca por internet público).
Verifica que el secreto fue establecido:
fly secrets list
# NOMBRE DIGEST CREADO EN
# DATABASE_URL abc123 2025-02-21T10:00:00ZCadena de Conexión para EF Core
El formato DATABASE_URL de Fly es una URI libpq. Npgsql la acepta directamente:
// Parsear DATABASE_URL del formato de 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, // La red interna de Fly ya está encriptada via WireGuard
}.ToString();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
}Ejecutando Migraciones de EF Core en el Despliegue
El enfoque más limpio es un release_command en fly.toml — Fly lo ejecuta antes de enrutar tráfico a las nuevas máquinas:
[deploy]
release_command = "dotnet MyApi.dll migrate"Agrega un manejador de comando personalizado en Program.cs:
// Verificar args de CLI antes de construir la app completa
if (args.Contains("migrate"))
{
// Construir un host mínimo solo para migración
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("Migraciones aplicadas exitosamente.");
return;
}
// El inicio normal de la app continúa abajo...Escalado Horizontal y Multi-Región
Escalar Dentro de una Región
# Ejecutar 3 máquinas en la región primaria (iad)
fly scale count 3
# Verificar
fly statusEl balanceador de carga de Fly.io distribuye solicitudes entre todas las máquinas sanas usando least-connections.
Despliegue Multi-Región
# Agregar máquinas en Frankfurt y Tokio
fly scale count 1 --region fra
fly scale count 1 --region nrt
# Verificar la distribución
fly statusTu fly.toml puede anclar ciertas regiones:
# Mantener al menos una máquina en cada región en todo momento
[[regions]]
code = "iad"
count = 2
[[regions]]
code = "lhr"
count = 1Para APIs .NET con EF Core y un único clúster Postgres, ten cuidado con las escrituras multi-región. El Postgres gestionado de Fly no replica automáticamente las escrituras a regiones réplica — todas las escrituras van al primario. Usa headers fly-replay o enruta endpoints con muchas escrituras a la región primaria.
Dominios Personalizados y TLS
Fly.io provisiona certificados TLS automáticamente via Let's Encrypt.
# Agregar un dominio personalizado (debes ser propietario)
fly certs add api.tudominio.com
# Verificar estado del certificado
fly certs show api.tudominio.comLa salida incluye dos registros DNS para agregar en tu registrador:
Tipo Host Valor
A api.tudominio.com 66.241.124.x
AAAA api.tudominio.com 2a09:8280:1::...O usa un CNAME apuntando a myapi-prod.fly.dev. Los certificados se emiten en minutos después de la propagación DNS.
Comportamiento del Cold Start
Con auto_stop_machines = true y min_machines_running = 0, Fly.io detiene tu máquina después de ~5 minutos sin tráfico. La siguiente solicitud dispara un arranque en frío.
Línea de tiempo de cold start para una API .NET:
| Fase | Duración Típica |
|---|---|
| Boot de VM Fly.io (Firecracker) | ~300 ms |
| Pull de imagen Docker (primer despliegue) | ~0 ms (imagen local al host) |
| Inicialización del runtime .NET | ~200–600 ms |
| Pipeline de middleware ASP.NET | ~50–100 ms |
| Total | ~550–1000 ms |
Esto es rápido comparado con los cold starts de .NET en AWS Lambda, pero aún notable para APIs interactivas.
Cómo Evitar Cold Starts
Opción 1: Establecer min_machines_running = 1
[http_service]
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1 # Mantener siempre una máquina calienteEsto cuesta ~$1.94/mes para una máquina shared-cpu-1x — esencialmente gratis para producción.
Opción 2: Startup más rápido con Native AOT de .NET
<!-- MyApi.csproj — habilita compilación Native AOT -->
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>Las apps .NET compiladas con AOT arrancan en ~50–100 ms en total. La contrapartida: tiempos de build más largos, sin reflexión en runtime y algunos paquetes NuGet no son compatibles con AOT.
Opción 3: Solicitud de calentamiento via health check
Configura un monitor externo de uptime (ej.: BetterUptime, capa gratuita de UptimeRobot) para hacer ping a /health cada 4 minutos. Esto mantiene la máquina activa sin pagar por min_machines_running.
Precios
Los precios de Fly.io (a 2026) son basados en uso: pagas por segundo de ejecución de máquina, no por la capacidad provisionada. Las máquinas detenidas no acumulan cargos de cómputo (sigues pagando por los volúmenes y las direcciones IPv4 reservadas).
Cómputo (CPU Compartida)
| Tamaño de Máquina | vCPU | RAM | Precio/Mes (completo) |
|---|---|---|---|
| shared-cpu-1x | 1 compartida | 256 MB | ~$1.94 |
| shared-cpu-1x | 1 compartida | 512 MB | ~$3.13 |
| shared-cpu-2x | 2 compartidas | 512 MB | ~$5.70 |
| shared-cpu-4x | 4 compartidas | 1 GB | ~$10.70 |
Cómputo (CPU Dedicada)
| Tamaño de Máquina | vCPU | RAM | Precio/Mes |
|---|---|---|---|
| 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
| Plan | RAM | Almacenamiento | Precio/Mes |
|---|---|---|---|
| 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 |
Prueba Gratuita, No un Tier Gratuito
Fly.io ya no ofrece una allowance gratuita permanente. Las organizaciones nuevas reciben un crédito de prueba único, y el registro requiere una tarjeta de crédito — si no puedes o no quieres añadir una, Fly.io queda descartado, y el tier gratuito de Render es la alternativa más cercana. Las cuentas creadas antes del cambio de precios pueden conservar allowances heredadas.
Dicho eso, el scale-to-zero mantiene los costos reales mínimos: una máquina shared-cpu-1x con auto_stop_machines = true que solo corre cuando llega tráfico factura centavos al mes para una API de bajo tráfico.
Revisa el acumulado del mes actual en el dashboard de Fly.io en Billing, y configura una alerta de gasto para evitar sorpresas.
Flujo Completo de Despliegue
Aquí está la secuencia de principio a fin desde cero hasta producción:
# 1. Instalar flyctl y autenticarse
curl -L https://fly.io/install.sh | sh
fly auth login
# 2. Crear la app (genera fly.toml)
fly launch --name myapi-prod --region iad --no-deploy
# 3. Establecer secretos de la aplicación
fly secrets set \
JWT_SECRET="$(openssl rand -base64 32)" \
ENVIRONMENT="Production"
# 4. Crear y adjuntar Postgres
fly postgres create --name myapi-db --region iad --vm-size shared-cpu-1x
fly postgres attach --app myapi-prod myapi-db
# 5. Desplegar
fly deploy
# 6. Verificar
fly status
fly logs
# 7. Abrir en el navegador
fly openPros y Contras para APIs .NET
Pros
| Aspecto | Detalle |
|---|---|
| Anycast Global | Enruta usuarios a la región más cercana con una sola IP — sin configuración de CDN |
| Firecracker VMs | Mejor aislamiento que contenedores compartidos; latencia predecible |
| Red privada | El tráfico app-a-Postgres permanece en la malla WireGuard, nunca es público |
| Soporte Docker .NET | Las imágenes oficiales de Microsoft funcionan perfectamente; sin fricciones con buildpacks |
| Rolling deploys | Zero-downtime de fábrica, sin YAML de Kubernetes |
| Acceso SSH | fly ssh console da una shell real para debugging |
| Precio | Shared-cpu-1x a $1.94/mes es difícil de superar para APIs pequeñas |
| Scale-to-zero | Las máquinas detenidas no facturan nada — las apps ociosas cuestan centavos al mes |
Contras
| Aspecto | Detalle |
|---|---|
| Cold starts | Scale-to-zero significa ~1s de cold start; no ideal para APIs con SLA estrictos |
| Sin SQL Server gestionado | Fly ofrece Postgres y Redis; si necesitas SQL Server, lo ejecutas tú mismo |
| Fly Postgres es semi-DIY | Es Postgres en una VM, no un servicio completamente gestionado como RDS — gestionas extensiones y backups manualmente |
| Gestión de volúmenes | Los volúmenes persistentes están bloqueados por región; las apps con estado multi-región son complejas |
| Sin APM integrado | Sin equivalente a Application Insights; integra OpenTelemetry + un proveedor externo |
| Ecosistema más pequeño | Menos tutoriales, menos cobertura en StackOverflow que AWS/Azure |
| Configuración de WireGuard | Conectarse desde tu máquina local a servicios internos de Fly requiere fly proxy o un cliente WireGuard |
Resumen
| Escenario | Recomendación |
|---|---|
| Proyecto personal / side project | El scale-to-zero mantiene el costo casi en cero — pero el registro requiere tarjeta de crédito |
| API de producción pequeña (<100 req/s) | shared-cpu-1x, min_machines_running = 1, Fly Postgres |
| API global (baja latencia mundial) | Despliegue multi-región con 3–5 máquinas |
| API de alto tráfico (>500 req/s) | Máquinas performance-1x + clúster Postgres HA |
| Requisitos empresariales / compliance | AWS/Azure (más herramientas de auditoría, SLAs, SQL Server nativo) |
| Equipo ya en Kubernetes | Considera Fly.io para servicios más pequeños; apps más grandes pueden superar sus límites |
Fly.io alcanza un punto óptimo para desarrolladores .NET que quieren despliegues nativos en Docker, presencia global y precios transparentes sin la complejidad de AWS ECS/EKS ni la sobrecarga de configuración de Azure App Service. La CLI flyctl es genuinamente placentera de usar, la red privada basada en WireGuard es una ventaja de seguridad sobre el peering VPC tradicional, y el aislamiento de Firecracker VM significa que tu contenedor se comporta como una máquina real. El principal problema para .NET es la oferta de base de datos gestionada solo con Postgres — si tu app requiere SQL Server, lo estarás ejecutando en una VM simple.