Railway se ha convertido en la plataforma preferida de los desarrolladores que quieren la simplicidad de Heroku con infraestructura moderna. Para los desarrolladores .NET, es el camino más rápido desde un repositorio de GitHub hasta una URL de producción en funcionamiento. Esta guía recorre un despliegue completo y real — la app, el Dockerfile y el railway.json usados aquí viven en
samples/dotnet-hosting, una minimal API de .NET 10 que puedes desplegar tú mismo — incluyendo el fallo que verás si tu app no está en la raíz del repositorio, y las dos configuraciones que lo arreglan.
Lo que Necesitas
- Un proyecto de ASP.NET Core en GitHub (cualquier versión soportada — el ejemplo usa .NET 10)
- Una cuenta de Railway (prueba gratuita en railway.app)
- Railway CLI (opcional pero recomendado)
Configuración del Dockerfile
Railway puede detectar automáticamente proyectos .NET con su builder Railpack, pero un Dockerfile te da control total sobre el proceso de construcción. Este es el que usa el ejemplo:
# 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"]Railway inyecta el puerto de escucha como variable de entorno PORT en tiempo de ejecución. La tentadora línea de Dockerfile ENV ASPNETCORE_URLS=http://+:${PORT:-8080} NO funciona — Docker resuelve ${...} cuando la imagen se construye, así que 8080 queda fijado y el valor inyectado por Railway se ignora silenciosamente. Lee PORT en Program.cs en su lugar.
// Program.cs — leer el PORT que Railway 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).El Archivo de Configuración railway.json
Railway se puede configurar por completo desde el dashboard, pero un archivo de configuración versionado mantiene los ajustes junto al código. Este es el deploy/railway.json del ejemplo:
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"healthcheckPath": "/health",
"healthcheckTimeout": 300,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}No hace falta startCommand — el ENTRYPOINT del Dockerfile ya lo cubre. La ruta del health check necesita un endpoint correspondiente en tu app:
// Program.cs
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));Railway solo detecta automáticamente railway.json en la raíz del servicio. El ejemplo lo guarda bajo deploy/, así que el recorrido de abajo se lo indica a Railway explícitamente — un solo campo de configuración.
Despliegue desde GitHub (Sin CLI)
- Ve a railway.app e inicia sesión (el login con GitHub también gestiona la autorización del repositorio)
- Haz clic en New Project → Deploy from GitHub repo
- Selecciona tu repositorio y Railway comienza a construir de inmediato
Si tu repositorio es una sola app con el Dockerfile en la raíz, eso es todo — en ~2 minutos tienes una URL activa y puedes saltar a generar el dominio.
Cuando la App No Está en la Raíz del Repositorio
El ejemplo vive en un monorepo (samples/dotnet-hosting dentro de dotnet-samples), y la primera construcción falla exactamente como fallará la tuya: Railpack escanea la raíz del repositorio, encuentra archivos de solución pero ninguna app construible, y se rinde.

Dos ajustes lo arreglan. En la pestaña Settings del servicio, establece Root Directory a la carpeta que contiene tu app:

Y como el railway.json del ejemplo está en una subcarpeta deploy/ en lugar de la raíz del servicio, apunta Config-as-code hacia él:

Vuelve a desplegar, y el builder cambia de Railpack al Dockerfile. Los logs de despliegue muestran la app arrancando, escuchando en el puerto inyectado por Railway y respondiendo con un 200 al health check /health del railway.json antes de que el despliegue entre en producción:

Generar una URL Pública
Desplegado no significa público — los servicios de Railway no reciben URL externa por defecto. En Settings → Networking, haz clic en Generate Domain (acepta el puerto 8080):

El ejemplo informa en qué plataforma aterrizó detectando variables de entorno específicas de cada plataforma (RAILWAY_ENVIRONMENT, en este caso):
![Navegador mostrando la respuesta JSON de dotnet-samples-production.up.railway.app: service JorgenHoc hosting sample, runtime 10.0.11, platform Railway, listeningOn http://[::]:8080.](/images/deploy-dotnet-railway/deploy-dotnet-railway-deployed-app-response.png)
Railway CLI
La CLI te ofrece desarrollo local, streaming de logs y gestión de entornos:
# Instalar
npm install -g @railway/cli
# Autenticar
railway login
# Vincular a un proyecto existente (desde el directorio de tu proyecto)
railway link
# O crear un nuevo proyecto
railway init
# Desplegar el directorio actual
railway up
# Ver logs en tiempo real
railway logs
# Abrir la app en ejecución en el navegador
railway openDesplegando con la CLI
# Despliegue con un solo comando
railway up
# Desplegar un servicio específico
railway up --service my-api
# Desplegar y ver logs
railway up && railway logsVariables de Entorno
Las variables de entorno de Railway se configuran por servicio y por entorno (Production, Staging, etc.):
Mediante el Dashboard
En el dashboard de Railway: Service → Variables → Add variable
Mediante la CLI
# Establecer variables
railway variables --set "ASPNETCORE_ENVIRONMENT=Production" --set "JWT_SECRET=your-secret-here"
# Listar todas las variables
railway variablesAcceso desde tu App .NET
Las variables de Railway son simples variables de entorno — se integran perfectamente con la configuración de ASP.NET Core:
// Los valores de appsettings.json son sobrescritos por las variables de entorno
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Esto lee de la variable de entorno ConnectionStrings__DefaultConnection (nota el doble guión bajo)
// O leer directamente
var jwtSecret = builder.Configuration["JWT_SECRET"]
?? throw new InvalidOperationException("JWT_SECRET not configured");Añadir una Base de Datos PostgreSQL
Railway tiene un servicio nativo de Postgres que se conecta automáticamente a tu app:
# Añadir Postgres a tu proyecto
railway add --database postgres
# Railway establece automáticamente DATABASE_URL en el entorno de tu servicioInstala el proveedor de PostgreSQL para EF Core:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL// Program.cs — leer la DATABASE_URL que establece Railway
var databaseUrl = builder.Configuration["DATABASE_URL"]
?? throw new InvalidOperationException("DATABASE_URL not set");
// Analizar el formato de URL postgres:// de Railway
var databaseUri = new Uri(databaseUrl);
var userInfo = databaseUri.UserInfo.Split(':');
var connectionString = $"Host={databaseUri.Host};Port={databaseUri.Port};" +
$"Database={databaseUri.AbsolutePath.TrimStart('/')};" +
$"Username={userInfo[0]};Password={userInfo[1]};" +
$"SSL Mode=Require;Trust Server Certificate=true";
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));O usa el constructor de cadenas de conexión de Npgsql:
# Railway también proporciona variables de conexión individuales:
PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORDvar connectionString = new NpgsqlConnectionStringBuilder
{
Host = builder.Configuration["PGHOST"],
Port = int.Parse(builder.Configuration["PGPORT"] ?? "5432"),
Database = builder.Configuration["PGDATABASE"],
Username = builder.Configuration["PGUSER"],
Password = builder.Configuration["PGPASSWORD"],
SslMode = SslMode.Require
}.ConnectionString;Dominios Personalizados
# Añadir un dominio personalizado mediante la CLI
railway domain www.yourdomain.com
# Railway proporciona el CNAME para apuntar en tu registrador de dominioO mediante el dashboard: Service → Settings → Networking → Custom Domain (junto al botón Generate Domain mostrado antes).
Railway proporciona certificados TLS automáticamente para todos los dominios personalizados.
Entornos (Staging / Production)
Railway admite múltiples entornos por proyecto:
# Crear un entorno de staging
railway environment new staging
# Cambiar el contexto de la CLI a staging (railway up apuntará a él)
railway environment stagingCada entorno tiene su propio conjunto de variables, bases de datos y despliegues. Tu entorno de staging puede replicar producción con una base de datos separada.
Precios
Railway utiliza un modelo de precios basado en el uso:
| Plan | Base Mensual | Cómputo | Memoria |
|---|---|---|---|
| Trial | $5 de crédito (una vez) | $0,000463/vCPU-minuto | $0,000231/GB-minuto |
| Hobby | $5/mes incluidos | Mismas tarifas | Mismas tarifas |
| Pro | $20/mes incluidos | Mismas tarifas | Mismas tarifas |
Costos típicos para una pequeña API .NET:
Una API .NET mínima que usa ~0,1 vCPU y 256 MB de RAM de forma continua:
- Cómputo: 0,1 × 43 800 min × $0,000463 ≈ $2,03/mes
- Memoria: 0,25 × 43 800 min × $0,000231 ≈ $2,53/mes
- Total: ~$4,56/mes (dentro del crédito de $5 del plan Hobby)
Añadir Postgres: $0,000231/GB-minuto por almacenamiento + cómputo de la instancia de BD.
La mayoría de las APIs .NET pequeñas con una base de datos Postgres funcionan cómodamente dentro del plan Hobby de $5/mes de Railway. Estima tus costos en railway.app/pricing antes de escalar.
Despliegues Automáticos
Por defecto, cada push a tu rama principal desencadena un despliegue. Configura los despliegues basados en ramas:
En el dashboard: Service → Settings → Deployments:
- Watch Branch:
main(o cualquier rama) - Root Directory:
/o un subdirectorio si tu proyecto .NET no está en la raíz del repositorio
Pros y Contras
Pros
- Cero configuración para lo básico — sube el código, obtén la URL, sin manifiestos YAML
- Integración nativa con Git — despliegue automático en cada push
- Excelente DX — el dashboard es limpio e intuitivo
- Postgres, Redis, MySQL nativos — un clic para añadir una base de datos
- Precios justos — basados en el uso, fáciles de estimar
- Dominios personalizados con TLS automático — incluido en todos los planes
- Ramificación de entornos — separación staging/producción integrada
Contras
- Sin SLA en el plan Hobby — se requiere el plan Pro para garantías de disponibilidad
- Regiones centradas en EE. UU. — menos regiones que AWS/Azure (aunque en expansión)
- Funciones empresariales limitadas — sin VPCs ni redes privadas en los planes inferiores
- Los minutos de construcción pueden acumularse — las soluciones .NET grandes con muchos proyectos toman tiempo
- Sin autoescalado — solo escalado vertical (actualizar el tamaño de la instancia); el escalado horizontal es manual
Cuándo Elegir Railway
Railway es la elección correcta cuando:
- Eres un desarrollador independiente o un equipo pequeño
- Quieres desplegar rápidamente sin experiencia en infraestructura
- Tu app es una combinación estándar de web API + base de datos
- El presupuesto es una preocupación y quieres costos bajos y predecibles
- Estás haciendo un prototipo o gestionando un proyecto paralelo
Migra fuera de Railway cuando:
- Necesitas garantías de cumplimiento (SOC2, HIPAA con BAA, etc.)
- Necesitas redes complejas (VPCs, endpoints privados)
- Necesitas despliegues activo-activo en múltiples regiones
- Tus patrones de tráfico requieren autoescalado sofisticado