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

Despliega una App .NET en Railway en 10 Minutos

Guía paso a paso para desplegar una app .NET 10 en Railway: configuración de Dockerfile, archivo railway.json, root directory en monorepos, variables de entorno, dominios personalizados, Railway CLI, precios y pros y contras honestos.

#dotnet#cloud#devops

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)

  1. Ve a railway.app e inicia sesión (el login con GitHub también gestiona la autorización del repositorio)
  2. Haz clic en New ProjectDeploy from GitHub repo
  3. 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.

Log de construcción de Railway mostrando 'Deployment failed during build process': Railpack lista el contenido de la raíz del repositorio — shared/, .gitignore, Directory.Build.props, DotNetSamples.slnx, LICENSE, README.md — y no logra construir una imagen.
El primer fallo esperado en un monorepo: Railpack escanea la raíz del repositorio, no encuentra ninguna app que construir y se detiene.

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

Configuración de Source en Railway mostrando el repositorio conectado jorgenhoc-org/dotnet-samples y el campo Root Directory con el valor /samples/dotnet-hosting.
Settings → Source: Root Directory le dice a Railway dónde vive realmente la app (y su Dockerfile).

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:

Configuración Config-as-code de Railway con el campo Railway Config File establecido en /samples/dotnet-hosting/deploy/railway.json.
Settings → Config-as-code: la ruta explícita a railway.json, necesaria porque no está en la raíz del servicio.

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:

Logs de despliegue de Railway: el contenedor arranca, ASP.NET Core escucha en el puerto 8080 y el health check GET /health de Railway devuelve 200 en unos 140 ms.
Logs del despliegue exitoso: contenedor arriba, escuchando en 8080, y la sonda /health de Railway devolviendo 200 antes de enrutar tráfico.

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

Configuración de Networking en Railway mostrando el dominio público generado dotnet-samples-production.up.railway.app apuntando al puerto 8080, con los botones Generate Domain, Custom Domain y TCP Proxy.
Settings → Networking: un clic genera un dominio público *.up.railway.app con TLS incluido.

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.
La respuesta del ejemplo desplegado: runtime .NET 10, plataforma detectada como Railway mediante la variable RAILWAY_ENVIRONMENT.

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 open

Desplegando 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 logs

Variables 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 variables

Acceso 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 servicio

Instala 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, PGPASSWORD
var 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 dominio

O 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 staging

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

PlanBase MensualCómputoMemoria
Trial$5 de crédito (una vez)$0,000463/vCPU-minuto$0,000231/GB-minuto
Hobby$5/mes incluidosMismas tarifasMismas tarifas
Pro$20/mes incluidosMismas tarifasMismas 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

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