Railway builds and runs services from a local directory or a Git repository, provides a public HTTPS domain, and injects configuration through environment variables. It uses Railpack to detect Node, Python, PHP, Java, and other runtimes automatically, and falls back to a Dockerfile when one is present.
For an MCP server, the deployment comes down to three things: listen on 0.0.0.0 at the injected PORT, set the correct start command for the transport, and add your upstream credentials as variables.
Prerequisites
- A generated bundle for your chosen runtime.
- A Railway account and a project.
- The Railway CLI, or a GitHub repository connected to Railway.
Railway injects a PORT environment variable into every service. Its value is also used for health checks, so the process must bind to it rather than a hard-coded port.
1. Bind the server to Railway's port
TypeScript and Python use Railpack. For TypeScript, update the generated entry point to read PORT and listen on all interfaces.
// Before
port: { type: 'string', default: '3000' },
serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => {
// After
port: { type: 'string', default: process.env.PORT ?? '3000' },
serve({ fetch: app.fetch, port, hostname: '0.0.0.0' }, () => {
Make npm start launch the HTTP transport. Railpack runs this script after the build, so no separate start command is needed for TypeScript.
"scripts": {
"build": "tsc",
"start": "node dist/server.js --http",
"dev": "tsx watch src/server.ts"
}
Python needs no source changes. Set the start command in Railway to pass the host and the injected port:
python server.py --transport streamable-http --host 0.0.0.0 --port $PORT
Railpack runs custom start commands in a shell, so $PORT is expanded. PHP and Java use a Dockerfile instead; the ones in the Cloud Run guide already bind to 0.0.0.0 and read $PORT.
2. Deploy with the Railway CLI
The CLI can deploy the generated bundle directly, without pushing it to GitHub first. Install it, authenticate, and create a project from the bundle directory.
npm i -g @railway/cli
railway login
railway init
railway up
railway init creates and links a project. railway up uploads the current directory, builds it with Railpack, deploys it, and streams the logs. Add --detach if you do not want to follow the logs.
To deploy from Git instead, create a service from the Railway dashboard, connect the repository, and let each push trigger a deploy. The build and start settings are the same either way.
3. Set the build and start commands
Railpack usually detects the commands from the bundle. Open the service settings and confirm them; overriding is safer when you need the HTTP transport explicitly.
- TypeScript build: npm install && npm run build
- TypeScript start: npm start
- Python build: pip install -r requirements.txt
- Python start: python server.py --transport streamable-http --host 0.0.0.0 --port $PORT
Config files: Railway's older railway.json and railway.toml config-as-code format is deprecated, so this guide configures the service in the dashboard. The commands above work with Railpack on any current Railway account.
4. Add upstream credentials
The generated bundle reads credentials from environment variables prefixed with MCP_AUTH_. Add each variable under the service's Variables tab rather than committing secrets to the repository.
railway variable set MCP_AUTH_API_TOKEN=your-token
Setting a variable through the CLI can leave the value in your shell history. For production tokens, prefer the dashboard, and rotate any token that has been exposed. Railway injects the service variables into the running process, and a new deployment is required after a change.
5. Generate a public domain
Railway services are reachable over the private network by default. Generate a public domain from the service's Networking settings, or with the CLI.
railway domain
The command prints a URL like this. The MCP endpoint is that URL plus /mcp:
https://my-mcp-server-production.up.railway.app/mcp
Connect an MCP client that supports Streamable HTTP and call a read-only tool. Use railway logs to inspect build and runtime output.
railway logs
PHP and Java with Docker
Railway builds with a Dockerfile whenever it finds one in the bundle root. Copy the PHP or Java Dockerfile from the Cloud Run guide, then deploy the directory as usual. Do not set a custom start command: the Dockerfile's CMD already reads $PORT.
If you do override the start command for a Docker-based service, wrap it in a shell because Docker start commands use exec form and do not expand variables:
/bin/sh -c "exec php -S 0.0.0.0:$PORT server.php"
Health checks
Railway health checks request a path and expect a 2xx response before switching traffic to a new deployment. Do not point the path at /mcp, since a plain GET to the MCP endpoint may not return success.
Either leave the health check path empty, or add a small route to the server that returns 200 OK. For TypeScript, that is one line next to the MCP route:
app.get('/health', (c) => c.text('ok'));
Then set the health check path to /health in the service settings.
Usage and cost notes
- Usage-based billing. Railway charges for CPU, memory, and egress. The Free plan includes $1 of usage credit per month with one replica, 1 vCPU, and 0.5 GB RAM; the Hobby plan is $5 per month with $5 of included usage. Check current pricing for details.
- Serverless sleeping. Railway can put an idle service to sleep to reduce usage. It is not enabled by default, and the first request after sleep can be slow or return a 502 while the service boots. Enable it only if cold starts are acceptable for your MCP clients.
- Ephemeral filesystem. Container files are lost on redeploy or restart. The PHP bundle's file-based session store is not shared across replicas; use an external store for durable state.
- Edge limits. Railway allows HTTP requests up to 15 minutes while data keeps transferring, and closes connections after 5 minutes without data. Streamable HTTP servers that send keepalive events fit this model better than long idle streams.
- One replica on Free. Scaling to multiple replicas requires a paid plan. Multi-replica deployments need shared session state, because requests can land on any instance.
Operational notes
- Secure the domain. A generated Railway domain is publicly reachable. Upstream credentials are protected, but the MCP endpoint itself is not authenticated; add an authorizer, IP restrictions, or a gateway before sharing it.
- Redeploy on spec changes. Regenerate the bundle, reapply the port and start-command changes, and run railway up again, or push to the connected branch.
- Keep variables in sync. Each environment can have its own variable values. Confirm that production and any staging environment point at the intended upstream API.
- Watch the logs after deploy. Missing variables, port-binding errors, and dependency failures surface in railway logs before an MCP client can connect.
Next steps
Railway is a good fit when you want Git or CLI deploys, automatic runtime detection, and a public domain without writing infrastructure. Compare it with Render, Fly.io, Google Cloud Run, AWS App Runner, Vercel, and Cloudflare Workers. A side-by-side comparison is available in Where to Deploy Your MCP Server.
Need a bundle to deploy? Start with From OpenAPI to a Running MCP Server.