Render.com se ha convertido silenciosamente en una de las plataformas PaaS más amigables para desarrolladores que trabajan con cargas de trabajo en contenedores — pero la penalización de cold start del nivel gratuito y los saltos de precios sorprenden a los desarrolladores .NET que no saben en qué se están metiendo. Esta guía cubre todo, desde el Dockerfile hasta render.yaml, un despliegue real del ejemplo .NET 10 de
samples/dotnet-hosting, y una comparación honesta con Railway y Fly.io.
Qué es Render.com (y su posicionamiento en el mercado)
Render es una plataforma cloud completamente gestionada que ejecuta contenedores Docker, sitios estáticos, cron jobs y workers en segundo plano. Ocupa el punto intermedio entre Heroku (obsoleto o muy caro) y Kubernetes puro: obtienes deploys basados en Git, TLS gestionado y Postgres integrado sin escribir código de infraestructura.
Sus principales diferenciadores para desarrolladores .NET:
- HTTPS sin configuración en cada servicio y dominio personalizado
render.yaml— infraestructura como código versionada en tu repositorio- Postgres nativo con bases de datos gestionadas y backups automáticos
- Red privada entre servicios de la misma cuenta (sin costos de egress público)
- Auto-deploy en cada push a la rama que configures
El inconveniente: el nivel gratuito suspende los servicios inactivos, y el salto de gratuito ($0) a Starter ($7/mes) es la mejora más pequeña que elimina los cold starts.
Dockerfile para .NET en Render
Render ejecuta cualquier imagen Docker. El build multi-etapa estándar funciona sin modificaciones — este es el que usa el ejemplo en su despliegue:
# Dockerfile — multi-stage: la imagen SDK construye, la imagen ASP.NET (más pequeña) ejecuta
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["JorgenHoc.DotnetHosting.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "JorgenHoc.DotnetHosting.dll"]Render inyecta una variable de entorno PORT en tiempo de ejecución (10000 por defecto). La tentadora línea de Dockerfile ENV ASPNETCORE_URLS=http://+:${PORT:-8080} NO la recoge — Docker resuelve ${...} cuando la imagen se construye, así que el fallback queda fijado y el valor inyectado por Render se ignora silenciosamente. Lee PORT en Program.cs en su lugar.
// Program.cs — leer el PORT que Render inyecta en tiempo de ejecución
var port = Environment.GetEnvironmentVariable("PORT");
if (!string.IsNullOrEmpty(port))
{
builder.WebHost.UseUrls($"http://+:{port}");
}
// Sin PORT, aplica el puerto por defecto de la imagen base aspnet (8080).Despliegue desde el Dashboard
El camino más rápido es el formulario del dashboard — sin YAML. New → Web Service, conecta el repositorio de GitHub, y un solo formulario lo cubre todo. El único campo no obvio en un monorepo es Root Directory, que apunta el contexto de build de Docker a la carpeta correcta:

Elige un tipo de instancia (Free para este recorrido), haz clic en Deploy Web Service, y Render construye el Dockerfile y enruta tráfico una vez que pasa el health check:

El ejemplo desplegado informa la plataforma que detectó (mediante la variable de entorno RENDER) y el puerto que Render inyectó:
![Navegador mostrando la respuesta JSON de jorgenhoc-hosting-sample.onrender.com: service JorgenHoc hosting sample, runtime 10.0.11, platform Render, listeningOn http://[::]:10000.](/images/dotnet-hosting/render-app-response.png)
render.yaml — Infraestructura como Código
El formulario del dashboard funciona, pero render.yaml (un "Blueprint") declara servicios, bases de datos y variables de entorno en un archivo versionado en tu repo — Render lo aplica cuando conectas el repositorio como Blueprint. El ejemplo mantiene uno mínimo en deploy/render.yaml; un ejemplo más completo con forma de producción:
# render.yaml — versionado en control de código fuente
services:
- type: web
name: my-dotnet-api
runtime: docker
region: oregon # oregon | frankfurt | singapore | ohio
plan: starter # free | starter | standard | pro
branch: main # auto-deploy en cada push a esta rama
healthCheckPath: /health
# Overrides en tiempo de build (raramente necesario con un Dockerfile)
dockerfilePath: ./MyApi/Dockerfile
envVars:
- key: ASPNETCORE_ENVIRONMENT
value: Production
- key: ConnectionStrings__DefaultConnection
fromDatabase:
name: my-postgres-db # referencia al bloque de base de datos
property: connectionString
- key: JWT_SECRET
generateValue: true # Render genera un valor aleatorio una vez
autoDeploy: true
# Worker en segundo plano (sin puerto público, sin health check requerido)
- type: worker
name: my-background-worker
runtime: docker
plan: starter
branch: main
dockerfilePath: ./Worker/Dockerfile
envVars:
- key: ASPNETCORE_ENVIRONMENT
value: Production
databases:
- name: my-postgres-db
databaseName: myapp
user: myapp_user
plan: free # free | basic-256 | basic-512 | basic-1
region: oregonPuntos clave:
fromDatabase.property: connectionStringinyecta automáticamente el connection string completo de Postgres — sin copiar y pegar credenciales.generateValue: truees útil para secretos como las claves de firma JWT; Render los genera una vez y nunca los vuelve a mostrar en los logs.- El
healthCheckPathindica al balanceador de carga de Render qué endpoint consultar. Devuelve HTTP 200 cuando la app está lista.
Endpoint de Health Check
Agrega un endpoint de health mínimo para que Render sepa que tu app está activa:
// Program.cs
app.MapGet("/health", () => Results.Ok(new { status = "healthy", utc = DateTime.UtcNow }));Para producción, usa Microsoft.Extensions.Diagnostics.HealthChecks para incluir la conectividad con la base de datos:
builder.Services.AddHealthChecks()
.AddNpgsql(connectionString, name: "postgres");
// Mapear en /health
app.MapHealthChecks("/health");Variables de Entorno
Render provee dos formas de configurar variables de entorno:
1. render.yaml (versionado en código, valores no secretos)
envVars:
- key: ASPNETCORE_ENVIRONMENT
value: Production
- key: FEATURE_FLAG_NEW_CHECKOUT
value: "true"2. Dashboard (secretos, overrides por servicio)
Navega a tu servicio → Environment → Add Environment Variable. Los valores configurados en el dashboard sobreescriben render.yaml para la misma clave. Nunca pongas secretos en render.yaml — usa el dashboard o generateValue: true.
Render también expone variables integradas que puedes leer en tu app:
| Variable | Valor |
|---|---|
RENDER | true |
RENDER_SERVICE_NAME | Nombre de tu servicio |
RENDER_GIT_COMMIT | SHA completo del commit desplegado |
RENDER_GIT_BRANCH | Nombre de la rama |
PORT | Puerto al que debe enlazarse tu servidor |
// Leer contexto específico de Render para logging/tracing
var isRender = Environment.GetEnvironmentVariable("RENDER") == "true";
var commitSha = Environment.GetEnvironmentVariable("RENDER_GIT_COMMIT") ?? "local";
var serviceName = Environment.GetEnvironmentVariable("RENDER_SERVICE_NAME") ?? "unknown";
logger.LogInformation("Iniciando {Service} @ {Commit}", serviceName, commitSha[..7]);Agregar una Base de Datos Postgres
Via render.yaml (recomendado)
El bloque databases mostrado arriba crea una instancia de Postgres gestionada. Render la vincula a tu servicio mediante fromDatabase, que inyecta automáticamente la variable de entorno ConnectionStrings__DefaultConnection.
// appsettings.json — la clave debe coincidir con la clave envVar en render.yaml
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=myapp;Username=dev;Password=dev"
}
}// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));Via Dashboard
- Dashboard → New → PostgreSQL
- Elige la región (hazla coincidir con la región de tu servicio web para evitar latencia entre regiones)
- Copia la Internal Database URL — esta es la URL de la red privada, sin costo de egress
- Pégala en las variables de entorno de tu servicio como
ConnectionStrings__DefaultConnection
Siempre usa la Internal Database URL (comienza con postgres://...@dpg-...oregon-a:5432/...) en lugar de la URL externa. Las conexiones internas permanecen dentro de la red privada de Render — sin costos de egress y ~0.3ms menos de latencia vs el endpoint público.
Ejecutar Migraciones de EF Core en el Deploy
Render no tiene un hook de migración integrado. Enfoques comunes:
Opción 1 — Migrar al inicio (simple, adecuado para equipos pequeños)
// Program.cs — ejecutar migraciones antes de que la app empiece a aceptar peticiones
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// ApplyMigrations es idempotente — seguro ejecutarlo en cada inicio
await db.Database.MigrateAsync();
}Opción 2 — Job previo al deploy (más seguro para producción)
Agrega un preDeployCommand en render.yaml (actualmente en beta para servicios Docker). Como alternativa, usa la función de jobs puntuales de Render para ejecutar migraciones manualmente antes de promover.
Dominios Personalizados y TLS
- Dashboard → tu servicio → Settings → Custom Domains → Add Custom Domain
- Agrega el registro CNAME que requiere tu proveedor DNS (Render muestra el valor exacto)
- Render aprovisiona un certificado de Let's Encrypt automáticamente — generalmente en 60 segundos
- La redirección de HTTP a HTTPS está habilitada por defecto
Puedes agregar múltiples dominios personalizados por servicio. Los certificados wildcard (*.tudominio.com) no están soportados en el nivel gratuito.
Auto-Deploy desde GitHub
render.yaml establece autoDeploy: true y branch: main. Cada push a main dispara un nuevo build Docker y un deploy progresivo.
Para desplegar entornos de previsualización por pull request, habilita Pull Request Previews en la configuración de tu servicio. Cada PR obtiene su propia URL (https://my-dotnet-api-pr-42.onrender.com) con variables de entorno aisladas.
Para entornos de staging basados en ramas, crea un segundo servicio en render.yaml:
services:
- type: web
name: my-dotnet-api-staging
runtime: docker
plan: starter
branch: develop # auto-deploy staging desde la rama develop
envVars:
- key: ASPNETCORE_ENVIRONMENT
value: StagingEl Problema del Cold Start en el Nivel Gratuito
Esta es la queja más común sobre Render y lo que más importa antes de elegir un plan.
Qué ocurre: Los servicios web del nivel gratuito se suspenden después de 15 minutos de inactividad. La siguiente petición HTTP despierta el contenedor. Durante el proceso de activación, Render debe:
- Obtener la imagen Docker (en caché, rápido)
- Iniciar el contenedor
- Esperar a que tu app supere su health check
Para una API .NET típica, esto tarda 25–60 segundos. El inicio de ASP.NET no es rápido — escanea ensamblados, inicializa DI, calienta EF Core y conecta a Postgres antes de aceptar tráfico.
La primera petición después de un cold start o bien cuelga durante 30–60 segundos o devuelve un 502 (si el timeout del health check es menor que el tiempo de inicio de tu app).
Midiendo Tu Cold Start
Agrega timing de inicio para entender dónde se invierte el tiempo:
// Program.cs
var sw = System.Diagnostics.Stopwatch.StartNew();
var builder = WebApplication.CreateBuilder(args);
// ... registro de servicios ...
var app = builder.Build();
// ... pipeline de middleware ...
app.Lifetime.ApplicationStarted.Register(() =>
{
sw.Stop();
// Log a stdout — visible en el visor de logs de Render
Console.WriteLine($"[STARTUP] Listo en {sw.ElapsedMilliseconds}ms");
});
await app.RunAsync();Soluciones al Problema de Cold Start
Opción 1 — Ping externo de health check (gratuito)
Usa un monitor de uptime gratuito (UptimeRobot, Better Uptime, nivel gratuito de Cronitor) para hacer ping a tu endpoint /health cada 10 minutos. Esto mantiene el contenedor activo sin necesidad de actualizar.
Limitaciones: el propio monitor puede tener intervalos sin actividad, y los pings no cuentan como tráfico de usuarios — el temporizador de inactividad de Render se basa en peticiones HTTP de clientes externos, no de tu propia monitorización.
Opción 2 — Actualizar a Starter ($7/mes)
El plan Starter elimina los cold starts por completo. El servicio se ejecuta continuamente, y obtienes 512 MB de RAM y una CPU compartida. Para la mayoría de las APIs .NET, esta es la elección correcta en el momento en que tienes usuarios reales.
Opción 3 — Optimizar el tiempo de inicio
Reduce la duración del cold start aplazando la inicialización costosa:
// Usar IHostedService para inicialización en segundo plano en lugar de bloquear el inicio
builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddHostedService<MyServiceWarmup>();
// MyServiceWarmup.cs
public class MyServiceWarmup : IHostedService
{
private readonly IMyService _service;
public MyServiceWarmup(IMyService service) => _service = service;
public async Task StartAsync(CancellationToken ct)
{
// Ejecutar init costoso en segundo plano — no bloquea el health check
_ = Task.Run(() => _service.WarmUpAsync(ct), ct);
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}Si tu app tarda más que el timeout del health check de Render (por defecto 180 segundos) en iniciar, el deploy fallará y Render revertirá a la versión anterior. Aumenta el timeout en Service Settings → Health Check Timeout, u optimiza el inicio.
Precios
| Plan | Precio/mes | RAM | CPU | Cold Starts | Ideal Para |
|---|---|---|---|---|---|
| Free | $0 | 512 MB | Compartida | Sí (15 min inactivo) | Demos, proyectos personales |
| Starter | $7 | 512 MB | Compartida | No | Proyectos personales, bajo tráfico |
| Standard | $25 | 2 GB | 1 vCPU | No | APIs en producción |
| Pro | $85 | 4 GB | 2 vCPU | No | Servicios de alto tráfico |
| Pro Plus | $175 | 8 GB | 4 vCPU | No | Cargas de trabajo con uso intensivo de CPU |

Precios de Postgres (separados del servicio web):
| Plan | Precio/mes | Almacenamiento | RAM |
|---|---|---|---|
| Free | $0 | 256 MB | 256 MB |
| Basic-256 | $7 | 256 MB | 1 GB |
| Basic-512 | $14 | 512 MB | 1 GB |
| Basic-1 | $28 | 1 GB | 2 GB |
| Pro | $65 | 100 GB | 4 GB |
El Postgres gratuito expira 30 días después de su creación (sin backups) — después Render elimina la base de datos. Para datos gratuitos persistentes, usa un proveedor externo como Neon; para producción, usa al menos Basic-256.
Ancho de banda: 100 GB gratuitos al mes entre todos los servicios; $0.10/GB después.
Costo mensual estimado para una API .NET típica + Postgres:
- Desarrollo: Web gratuito + Postgres gratuito = $0 (cold starts, límite de DB de 30 días)
- Producción: Web Starter + Postgres Basic-256 = $14/mes
- Producción escalada: Web Standard + Postgres Basic-1 = $53/mes
Benchmark: Render vs Railway (Números Reales)
Estos números se midieron de verdad (agosto de 2026) contra los dos despliegues activos de esta serie de artículos — la API .NET 10 de ejemplo en Render Free (Oregon) y Railway Hobby (US East). Fly.io estaba planeado como tercera plataforma pero no está medido: ya no tiene tier gratuito y el registro requiere tarjeta de crédito.
Configuración de prueba:
- Herramienta: k6, script en
benchmark/k6-latency.js— ejecútalo contra tus propios despliegues - Carga: tasa de llegada fija de 30 req/s durante 60 segundos (~1,800 peticiones por plataforma), servicios calentados primero
- Endpoint:
/, un payload JSON pequeño, sin base de datos — esto mide la ruta HTTP de la plataforma, no el rendimiento de consultas - Cliente: una sola máquina en Bogotá, Colombia. Con una tasa de llegada fija, la distribución de latencia está dominada por la ruta de red entre el cliente y la región del proveedor — estos números te dicen lo que experimenta un usuario en esa ubicación, no una velocidad absoluta de la plataforma. Vuelve a ejecutar el script desde donde están tus usuarios.
Latencia en Caliente
| Proveedor | Plan | Región | p50 | p95 | p99 | Peticiones fallidas |
|---|---|---|---|---|---|---|
| Render | Free ($0) | Oregon | 152 ms | 206 ms | 318 ms | 0 de 1,800 |
| Railway | Hobby ($5) | US East | 181 ms | 220 ms | 312 ms | 0 de 1,801 |
La sorpresa: Render resultó más rápido en la mediana a pesar de que su origen está más lejos (Oregon vs US East). La razón es la red edge — Render está detrás de Cloudflare, que terminó TLS en Bogotá, mientras que el edge más cercano de Railway era Miami. En el p99 son indistinguibles. La conclusión honesta: a 30 req/s en los tiers más baratos, ambas plataformas van sobradas, y la geografía de tus usuarios importa más que el cómputo de la plataforma.
Cold Start (primera petición tras 22 minutos inactivo)
| Proveedor | 1.ª petición | 2.ª petición | 3.ª petición |
|---|---|---|---|
| Render Free | 12.3 s | 0.63 s | 0.45 s |
| Railway Hobby | 0.69 s | 0.45 s | 0.47 s |
Render Free se había suspendido y necesitó 12.3 segundos para arrancar el contenedor, iniciar .NET y pasar el health check. Ojo: esta minimal API es el mejor caso — sin grafo de DI, sin calentamiento de EF Core, sin conexión a base de datos. Una aplicación real queda más cerca del rango de 25–60 segundos descrito antes, y por eso el propio banner del dashboard de Render advierte retrasos de "50 segundos o más". Railway Hobby nunca duerme, así que su número "frío" es solo un handshake TLS nuevo.
Conclusiones clave:
- Ambos tiers más baratos manejaron 30 req/s con cero peticiones fallidas y un p99 por debajo de 330 ms desde Sudamérica.
- Las redes edge vencen a la proximidad del origen en la mediana: Render + Cloudflare fue más rápido desde Bogotá que la región de origen más cercana de Railway.
- La verdadera diferencia del tier gratuito es el cold start: Render Free paga 12+ segundos tras cada período inactivo; Railway no tiene tier web gratuito, así que nada se duerme jamás.
- Fly.io no está medido aquí — si tienes cuenta, despliega el mismo ejemplo con su
fly.tomly ejecuta el mismo script.
Pros y Contras
Pros
- render.yaml es excelente — la historia de infra-as-code más limpia de las tres plataformas. Define todo en un archivo, diferéncialo en PRs.
- Postgres gestionado es de primera clase — backups diarios automáticos, recuperación point-in-time en planes superiores, inyección de connection string sin fricciones.
- Entornos de previsualización de PR — cada PR obtiene una URL live con configuración aislada. Enorme para flujos de trabajo de QA.
- Sin configuración de red — el service discovery interno simplemente funciona. Los workers pueden llamar a servicios web por nombre sin configuración adicional.
- Precios transparentes — sin sorpresas en la factura de egress. 100 GB/mes de ancho de banda gratuito cubre la mayoría de las apps pequeñas.
- La experiencia de desarrollo está pulida — los logs de deploy son en tiempo real, los rollbacks son con un clic, los diffs de variables de entorno son auditables.
Contras
- Los cold starts del nivel gratuito son brutales para .NET — activaciones de 30–60 segundos son peores que la mayoría de las plataformas debido a la sobrecarga de inicio de .NET. Aceptable para demos, inutilizable para usuarios reales.
- Los tiempos de build dependen de tu Dockerfile — estructúralo para aprovechar la caché de capas (restore antes de copy) o lo notarás en cada deploy.
- Sin edge/multi-región — Render tiene 4 regiones pero sin enrutamiento geo-automático ni distribución a nivel CDN. Fly.io gana aquí.
- CPU compartida en Starter es limitada — las apps .NET con tareas intensivas en CPU (generación de PDF, procesamiento de imágenes) se verán throttleadas en Starter y necesitarán Standard ($25).
- Sin soporte de GPU — no es una plataforma para cargas de trabajo de inferencia de ML.
- El Postgres gratuito expira a los 30 días — fácil de olvidar, doloroso cuando tu base de datos desaparece en producción.
Cuándo Elegir Render
| Escenario | Veredicto |
|---|---|
| Proyecto personal / demo | El nivel gratuito está bien — acepta los cold starts |
| Herramienta interna, bajo tráfico | Starter ($7) — mejor relación calidad-precio |
| API en producción, tráfico moderado | Standard ($25) — elección sólida |
| Necesitas latencia más cercana al usuario | Considera Fly.io en su lugar |
| Sin tarjeta de crédito / presupuesto de $0 estricto | Render Free es la única opción real de las tres |
| Infra-as-code completa en un archivo | Render gana con render.yaml |
| Postgres gestionado con backups | Render es excelente |
| Entornos de previsualización de PR | Render es el mejor de los tres |
Resumen
Render.com es una excelente opción para desarrolladores .NET que quieren una experiencia PaaS limpia sin gestionar infraestructura. El formato render.yaml es la mejor historia de infra-as-code de cualquier plataforma comparable, el Postgres gestionado es genuinamente bueno, y los previews de PR son un multiplicador de flujo de trabajo.
El nivel gratuito solo es adecuado para demos y desarrollo — el problema de cold start con .NET es demasiado severo para cualquier cosa orientada al usuario. Presupuesta $14/mes (Starter + Basic Postgres) como mínimo para una app en producción. A ese precio, Render es difícil de superar por la combinación de experiencia de desarrollo, fiabilidad y servicios gestionados.
Si la latencia bruta es tu principal restricción, ejecuta el script de k6 de arriba desde la región de tus usuarios — nuestras mediciones muestran que la geografía y las redes edge dominan a esta escala. Pero para la mayoría de los equipos .NET que despliegan una API web o un worker en segundo plano, Render ofrece todo lo que necesitas en un solo lugar.