//JorgenHoc
← Todos os artigos
.NET HostingPor Jorge CalderónAtualizado 10 min read

Faça o Deploy de uma App .NET no Railway em 10 Minutos

Guia passo a passo para fazer deploy de uma app .NET 10 no Railway: configuração do Dockerfile, arquivo railway.json, root directory em monorepos, variáveis de ambiente, domínios personalizados, Railway CLI, preços e prós e contras honestos.

#dotnet#cloud#devops

O Railway se tornou a plataforma preferida dos desenvolvedores que querem a simplicidade do Heroku com infraestrutura moderna. Para desenvolvedores .NET, é o caminho mais rápido de um repositório no GitHub até uma URL de produção em funcionamento. Este guia percorre um deploy completo e real — a app, o Dockerfile e o railway.json usados aqui vivem em samples/dotnet-hosting, uma minimal API em .NET 10 que você mesmo pode fazer deploy — incluindo a falha que você verá se sua app não estiver na raiz do repositório, e as duas configurações que a corrigem.

O que Você Precisa

  • Um projeto ASP.NET Core no GitHub (qualquer versão suportada — o exemplo usa .NET 10)
  • Uma conta no Railway (teste gratuito em railway.app)
  • Railway CLI (opcional, mas recomendado)

Configurando o Dockerfile

O Railway pode detectar automaticamente projetos .NET com seu builder Railpack, mas um Dockerfile oferece controle total sobre o processo de build. Este é o que o exemplo usa no deploy:

# Dockerfile — multi-stage: a imagem SDK faz o build, a imagem ASP.NET (menor) executa
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"]
⚠️

O Railway injeta a porta de escuta como variável de ambiente PORT em tempo de execução. A tentadora linha de Dockerfile ENV ASPNETCORE_URLS=http://+:${PORT:-8080} NÃO funciona — o Docker resolve ${...} quando a imagem é construída, então 8080 fica fixado e o valor injetado pelo Railway é silenciosamente ignorado. Leia PORT no Program.cs em vez disso.

// Program.cs — ler o PORT que o Railway injeta em tempo de execução
var port = Environment.GetEnvironmentVariable("PORT");
if (!string.IsNullOrEmpty(port))
{
    builder.WebHost.UseUrls($"http://+:{port}");
}
// Sem PORT, vale a porta padrão da imagem base aspnet (8080).

O Arquivo de Configuração railway.json

O Railway é totalmente configurável pelo dashboard, mas um arquivo de configuração versionado mantém os ajustes junto do código. Este é o deploy/railway.json do exemplo:

{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "DOCKERFILE",
    "dockerfilePath": "Dockerfile"
  },
  "deploy": {
    "healthcheckPath": "/health",
    "healthcheckTimeout": 300,
    "restartPolicyType": "ON_FAILURE",
    "restartPolicyMaxRetries": 3
  }
}

Não é preciso startCommand — o ENTRYPOINT do Dockerfile já cobre isso. O caminho do health check precisa de um endpoint correspondente na sua app:

// Program.cs
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));

O Railway só detecta automaticamente o railway.json na raiz do serviço. O exemplo o mantém em deploy/, então o passo a passo abaixo indica o caminho ao Railway explicitamente — um único campo de configuração.

Fazendo Deploy a Partir do GitHub (Sem CLI)

  1. Acesse railway.app e faça login (o login com GitHub também cuida da autorização do repositório)
  2. Clique em New ProjectDeploy from GitHub repo
  3. Selecione seu repositório e o Railway inicia o build imediatamente

Se o seu repositório é uma única app com o Dockerfile na raiz, é isso — em ~2 minutos você tem uma URL ativa e pode pular para a geração do domínio.

Quando a App Não Está na Raiz do Repositório

O exemplo vive em um monorepo (samples/dotnet-hosting dentro de dotnet-samples), e o primeiro build falha exatamente como o seu vai falhar: o Railpack escaneia a raiz do repositório, encontra arquivos de solução mas nenhuma app construível, e desiste.

Log de build do Railway mostrando 'Deployment failed during build process': o Railpack lista o conteúdo da raiz do repositório — shared/, .gitignore, Directory.Build.props, DotNetSamples.slnx, LICENSE, README.md — e não consegue construir uma imagem.
A primeira falha esperada em um monorepo: o Railpack escaneia a raiz do repositório, não encontra nenhuma app para construir e para.

Dois ajustes corrigem isso. Na aba Settings do serviço, defina Root Directory como a pasta que contém sua app:

Configurações de Source no Railway mostrando o repositório conectado jorgenhoc-org/dotnet-samples e o campo Root Directory com o valor /samples/dotnet-hosting.
Settings → Source: o Root Directory diz ao Railway onde a app (e seu Dockerfile) realmente vivem.

E como o railway.json do exemplo fica em uma subpasta deploy/ em vez da raiz do serviço, aponte o Config-as-code para ele:

Configuração Config-as-code do Railway com o campo Railway Config File definido como /samples/dotnet-hosting/deploy/railway.json.
Settings → Config-as-code: o caminho explícito para o railway.json, necessário porque ele não está na raiz do serviço.

Faça o redeploy, e o builder muda do Railpack para o Dockerfile. Os logs de deploy mostram a app iniciando, escutando na porta injetada pelo Railway e respondendo com 200 ao health check /health do railway.json antes de o deploy entrar em produção:

Logs de deploy do Railway: o contêiner inicia, o ASP.NET Core escuta na porta 8080 e o health check GET /health do Railway retorna 200 em cerca de 140 ms.
Logs do deploy bem-sucedido: contêiner no ar, escutando na 8080, e a sonda /health do Railway retornando 200 antes de rotear tráfego.

Gerando uma URL Pública

Deploy feito não significa público — os serviços do Railway não recebem URL externa por padrão. Em Settings → Networking, clique em Generate Domain (aceite a porta 8080):

Configurações de Networking do Railway mostrando o domínio público gerado dotnet-samples-production.up.railway.app apontando para a porta 8080, com os botões Generate Domain, Custom Domain e TCP Proxy.
Settings → Networking: um clique gera um domínio público *.up.railway.app com TLS incluído.

O exemplo informa em qual plataforma aterrissou detectando variáveis de ambiente específicas de cada plataforma (RAILWAY_ENVIRONMENT, neste caso):

Navegador mostrando a resposta JSON de dotnet-samples-production.up.railway.app: service JorgenHoc hosting sample, runtime 10.0.11, platform Railway, listeningOn http://[::]:8080.
A resposta do exemplo em produção: runtime .NET 10, plataforma detectada como Railway pela variável RAILWAY_ENVIRONMENT.

Railway CLI

A CLI oferece desenvolvimento local, streaming de logs e gerenciamento de ambientes:

# Instalar
npm install -g @railway/cli
 
# Autenticar
railway login
 
# Vincular a um projeto existente (a partir do diretório do seu projeto)
railway link
 
# Ou criar um novo projeto
railway init
 
# Fazer deploy do diretório atual
railway up
 
# Acompanhar logs em tempo real
railway logs
 
# Abrir a app em execução no navegador
railway open

Fazendo Deploy com a CLI

# Deploy com um único comando
railway up
 
# Fazer deploy de um serviço específico
railway up --service my-api
 
# Fazer deploy e acompanhar os logs
railway up && railway logs

Variáveis de Ambiente

As variáveis de ambiente do Railway são definidas por serviço e por ambiente (Production, Staging, etc.):

Via Dashboard

No dashboard do Railway: Service → Variables → Add variable

Via CLI

# Definir variáveis
railway variables --set "ASPNETCORE_ENVIRONMENT=Production" --set "JWT_SECRET=your-secret-here"
 
# Listar todas as variáveis
railway variables

Acessando na Sua App .NET

As variáveis do Railway são simples variáveis de ambiente — integram-se perfeitamente com a configuração do ASP.NET Core:

// Os valores de appsettings.json são sobrescritos por variáveis de ambiente
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Isso lê da variável de ambiente ConnectionStrings__DefaultConnection (note o duplo underscore)
 
// Ou ler diretamente
var jwtSecret = builder.Configuration["JWT_SECRET"]
    ?? throw new InvalidOperationException("JWT_SECRET not configured");

Adicionando um Banco de Dados PostgreSQL

O Railway tem um serviço nativo de Postgres que se conecta automaticamente à sua app:

# Adicionar Postgres ao seu projeto
railway add --database postgres
 
# O Railway define DATABASE_URL automaticamente no ambiente do seu serviço

Instale o provedor PostgreSQL para EF Core:

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
// Program.cs — ler a DATABASE_URL que o Railway define
var databaseUrl = builder.Configuration["DATABASE_URL"]
    ?? throw new InvalidOperationException("DATABASE_URL not set");
 
// Analisar o formato de URL postgres:// do 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));

Ou use o construtor de string de conexão do Npgsql:

# O Railway também fornece variáveis de conexão individuais:
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;

Domínios Personalizados

# Adicionar um domínio personalizado via CLI
railway domain www.yourdomain.com
 
# O Railway fornece o CNAME para apontar no seu registrador de domínio

Ou via dashboard: Service → Settings → Networking → Custom Domain (ao lado do botão Generate Domain mostrado antes).

O Railway fornece certificados TLS automaticamente para todos os domínios personalizados.

Ambientes (Staging / Production)

O Railway suporta múltiplos ambientes por projeto:

# Criar um ambiente de staging
railway environment new staging
 
# Mudar o contexto da CLI para staging (railway up passará a apontar para ele)
railway environment staging

Cada ambiente tem seu próprio conjunto de variáveis, bancos de dados e deploys. Seu ambiente de staging pode espelhar a produção com um banco de dados separado.

Preços

O Railway utiliza um modelo de preços baseado em uso:

PlanoBase MensalComputaçãoMemória
Trial$5 de crédito (uma vez)$0,000463/vCPU-minuto$0,000231/GB-minuto
Hobby$5/mês incluídosMesmas tarifasMesmas tarifas
Pro$20/mês incluídosMesmas tarifasMesmas tarifas

Custos típicos para uma pequena API .NET:

Uma API .NET mínima usando ~0,1 vCPU e 256 MB de RAM continuamente:

  • Computação: 0,1 × 43.800 min × $0,000463 ≈ $2,03/mês
  • Memória: 0,25 × 43.800 min × $0,000231 ≈ $2,53/mês
  • Total: ~$4,56/mês (dentro do crédito de $5 do plano Hobby)

Adicionar Postgres: $0,000231/GB-minuto por armazenamento + computação para a instância do banco.

💡

A maioria das APIs .NET pequenas com um banco de dados Postgres funciona confortavelmente dentro do plano Hobby de $5/mês do Railway. Estime seus custos em railway.app/pricing antes de escalar.

Deploys Automáticos

Por padrão, cada push na sua branch principal aciona um deploy. Configure deploys baseados em branches:

No dashboard: Service → Settings → Deployments:

  • Watch Branch: main (ou qualquer branch)
  • Root Directory: / ou um subdiretório se seu projeto .NET não estiver na raiz do repositório

Prós e Contras

Prós

  • Zero configuração para o básico — suba o código, obtenha a URL, sem manifestos YAML
  • Integração nativa com Git — deploy automático a cada push
  • Excelente DX — o dashboard é limpo e intuitivo
  • Postgres, Redis, MySQL nativos — um clique para adicionar um banco de dados
  • Preços justos — baseados em uso, fáceis de estimar
  • Domínios personalizados com TLS automático — incluído em todos os planos
  • Ramificação de ambientes — separação staging/produção integrada

Contras

  • Sem SLA no plano Hobby — plano Pro necessário para garantias de disponibilidade
  • Regiões centradas nos EUA — menos regiões que AWS/Azure (mas em expansão)
  • Recursos empresariais limitados — sem VPCs nem rede privada nos planos inferiores
  • Minutos de build podem se acumular — soluções .NET grandes com muitos projetos levam tempo
  • Sem auto-scaling — apenas escalonamento vertical (atualizar o tamanho da instância); escalonamento horizontal é manual

Quando Escolher o Railway

O Railway é a escolha certa quando:

  • Você é um desenvolvedor solo ou uma equipe pequena
  • Quer fazer deploy rapidamente sem experiência em infraestrutura
  • Sua app é uma combinação padrão de web API + banco de dados
  • O orçamento é uma preocupação e você quer custos baixos e previsíveis
  • Você está prototipando ou gerenciando um projeto paralelo

Migre para fora do Railway quando:

  • Precisar de garantias de conformidade (SOC2, HIPAA com BAA, etc.)
  • Precisar de redes complexas (VPCs, endpoints privados)
  • Precisar de deploys ativos-ativos em múltiplas regiões
  • Seus padrões de tráfego exigirem auto-scaling sofisticado

Leituras adicionais

Sobre o autor

Jorge Calderón

Engenheiro de software com mais de uma década construindo e operando aplicações .NET em produção — camadas de dados com EF Core, serviços intensivos em async e implantações em Azure e contêineres. Todos os benchmarks e projetos de exemplo destes guias estão publicados em um repositório público no GitHub para que você possa reproduzi-los.

Perfil no GitHubLinkedIn ↗Benchmarks e código de exemplo

Artigos relacionados