//JorgenHoc
← Todos los artículos
.NET HostingPor Jorge CalderónActualizado 17 min read

Hosting de una API .NET en Fly.io — Paso a Paso

Despliega tu API .NET en Fly.io: Dockerfile, configuración de fly.toml, comandos flyctl, gestión de secretos, addon de Postgres, dominios personalizados, precios y pros y contras honestos.

#dotnet#cloud#devops#docker

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 .internal en 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ónUbicación
iadAshburn, VA (EE.UU.)
ordChicago, IL (EE.UU.)
laxLos Ángeles, CA (EE.UU.)
lhrLondres, Reino Unido
fraFrankfurt, Alemania
nrtTokio, Japón
sydSídney, Australia
gruSã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 MyApi

La 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:local

Instalació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 flyctl

Autentícate:

fly auth login

Esto 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 launch

Se te pedirá:

  1. Nombre de la app — debe ser globalmente único entre todos los clientes de Fly.io (ej.: myapi-prod).
  2. Región primaria — elige la región más cercana a tus usuarios.
  3. Base de datos Postgres — recházalo aquí; lo configuraremos por separado más abajo.
  4. ¿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 = 256
⚠️

auto_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

ComandoQué Hace
fly launchAsistente interactivo de primer despliegue
fly deployConstruye imagen y despliega; usa el daemon Docker local por defecto
fly deploy --remote-onlyConstruye en el builder remoto de Fly (no se necesita Docker local)
fly statusMuestra las máquinas en ejecución y su salud
fly logsSigue los logs en vivo de todas las máquinas
fly logs -i <machine-id>Logs de una máquina específica
fly ssh consoleAbre una shell dentro de una máquina en ejecución
fly scale count 3Escala a 3 máquinas en la región primaria
fly scale count 1 --region lhrAsegura 1 máquina en Londres
fly releasesLista todos los despliegues con tags de imagen
fly rollbackRevierte al release anterior
fly apps destroy myapi-prodElimina 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 logs

Fly.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_KEY

En 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_db

Adjuntar a tu App

# Adjunta el clúster Postgres a tu app y establece DATABASE_URL automáticamente
fly postgres attach --app myapi-prod myapi-db

attach 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:00Z

Cadena 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 status

El 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 status

Tu 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 = 1
💡

Para 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.com

La 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:

FaseDuració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 caliente

Esto 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áquinavCPURAMPrecio/Mes (completo)
shared-cpu-1x1 compartida256 MB~$1.94
shared-cpu-1x1 compartida512 MB~$3.13
shared-cpu-2x2 compartidas512 MB~$5.70
shared-cpu-4x4 compartidas1 GB~$10.70

Cómputo (CPU Dedicada)

Tamaño de MáquinavCPURAMPrecio/Mes
performance-1x1 dedicada2 GB~$7.69
performance-2x2 dedicadas4 GB~$15.38
performance-4x4 dedicadas8 GB~$30.77

Postgres

PlanRAMAlmacenamientoPrecio/Mes
shared-cpu-1x256 MB1 GB~$1.94
shared-cpu-1x256 MB10 GB~$3.44
performance-1x2 GB50 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 open

Pros y Contras para APIs .NET

Pros

AspectoDetalle
Anycast GlobalEnruta usuarios a la región más cercana con una sola IP — sin configuración de CDN
Firecracker VMsMejor aislamiento que contenedores compartidos; latencia predecible
Red privadaEl tráfico app-a-Postgres permanece en la malla WireGuard, nunca es público
Soporte Docker .NETLas imágenes oficiales de Microsoft funcionan perfectamente; sin fricciones con buildpacks
Rolling deploysZero-downtime de fábrica, sin YAML de Kubernetes
Acceso SSHfly ssh console da una shell real para debugging
PrecioShared-cpu-1x a $1.94/mes es difícil de superar para APIs pequeñas
Scale-to-zeroLas máquinas detenidas no facturan nada — las apps ociosas cuestan centavos al mes

Contras

AspectoDetalle
Cold startsScale-to-zero significa ~1s de cold start; no ideal para APIs con SLA estrictos
Sin SQL Server gestionadoFly ofrece Postgres y Redis; si necesitas SQL Server, lo ejecutas tú mismo
Fly Postgres es semi-DIYEs Postgres en una VM, no un servicio completamente gestionado como RDS — gestionas extensiones y backups manualmente
Gestión de volúmenesLos volúmenes persistentes están bloqueados por región; las apps con estado multi-región son complejas
Sin APM integradoSin equivalente a Application Insights; integra OpenTelemetry + un proveedor externo
Ecosistema más pequeñoMenos tutoriales, menos cobertura en StackOverflow que AWS/Azure
Configuración de WireGuardConectarse desde tu máquina local a servicios internos de Fly requiere fly proxy o un cliente WireGuard

Resumen

EscenarioRecomendación
Proyecto personal / side projectEl 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 / complianceAWS/Azure (más herramientas de auditoría, SLAs, SQL Server nativo)
Equipo ya en KubernetesConsidera 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.

Lecturas adicionales

Sobre el autor

Jorge Calderón

Ingeniero de software con más de una década construyendo y operando aplicaciones .NET en producción — capas de datos con EF Core, servicios intensivos en async y despliegues en Azure y contenedores. Cada benchmark y proyecto de ejemplo de estas guías está publicado en un repositorio público de GitHub para que puedas reproducirlo.

Perfil de GitHubLinkedIn ↗Benchmarks y código de ejemplo

Artículos relacionados