4 MIN READ

Deploying Medusa on Railway: The Fastest Production Setup

Railway is the shortest path from a Medusa repository to a production deployment that is actually correct. The full setup, including the worker service everyone forgets.

BY RAHUL MEHTAUPDATED
Illustration for “Deploying Medusa on Railway: The Fastest Production Setup” — Deployment

Of the platforms we have deployed Medusa to, Railway gets a correct production architecture up fastest. Managed Postgres and Redis, two services from one repository, pre-deploy commands for migrations — the shape you want, without writing infrastructure code.

This is the whole setup, in order.

The services

ServiceSourcePurpose
PostgresRailway pluginDatabase
RedisRailway pluginEvents, cache, workflow engine
`medusa-server`Your repoHTTP: Store API, Admin API, dashboard
`medusa-worker`Your repoScheduled jobs and subscribers

The storefront deploys separately, usually to Vercel.

Postgres and Redis

Add both from Railway's plugin catalogue. Each exposes connection variables you reference from the application services rather than copying:

text
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}

Reference syntax, not literals. When a plugin rotates credentials, referenced variables follow and pasted ones do not.

Enable backups on the Postgres service on day one, and restore one into a scratch database before launch. A backup you have never restored is a hypothesis.

The server service

Point it at your repository and set the commands:

railway.jsonjson
{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "NIXPACKS",
    "buildCommand": "yarn build"
  },
  "deploy": {
    "preDeployCommand": "npx medusa db:migrate",
    "startCommand": "cd .medusa/server && npx medusa start",
    "healthcheckPath": "/health",
    "healthcheckTimeout": 120,
    "restartPolicyType": "ON_FAILURE"
  }
}

preDeployCommand is the important line. Railway runs it once per deploy, before the new container takes traffic — which is exactly the migration semantics you want, and exactly what you do not get by putting migrations in the start command.

Variables:

text
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}
MEDUSA_WORKER_MODE=server
MEDUSA_BACKEND_URL=https://api.yourdomain.com
JWT_SECRET=<openssl rand -base64 32>
COOKIE_SECRET=<openssl rand -base64 32>
STORE_CORS=https://yourdomain.com
ADMIN_CORS=https://api.yourdomain.com
AUTH_CORS=https://yourdomain.com,https://api.yourdomain.com
NODE_OPTIONS=--max-old-space-size=2048
PORT=9000

That NODE_OPTIONS line is the fix for the build failure everyone hits — see Dockerising Medusa for why the admin build is memory-hungry.

The worker service

Add a second service from the same repository. Identical build, different runtime:

text
MEDUSA_WORKER_MODE=worker
DISABLE_MEDUSA_ADMIN=true

and no preDeployCommand — migrations should run once per deploy, from the server service only.

Skipping the worker is the most common Railway mistake. Without it, every scheduled job runs on every web replica, and the symptoms (duplicate emails, double syncs) look like application bugs. Why the split matters.

Domains

Attach a custom domain to the server service — api.yourdomain.com — and Railway provisions TLS. Then make sure MEDUSA_BACKEND_URL and the CORS variables name that domain rather than the generated *.up.railway.app one, or the dashboard will call the wrong origin and fail with a message that does not mention CORS.

The worker needs no domain. It serves nothing.

First deploy

bash
# 1. Deploy, wait for the health check to pass
# 2. Create an admin user via Railway's one-off command runner
npx medusa user -e admin@yourdomain.com -p <password>
# 3. Open https://api.yourdomain.com/app and log in
# 4. Create a publishable key under Settings for the storefront

The storefront

Deploy the Next.js storefront to Vercel rather than Railway — it is a static-heavy application and belongs on a CDN-first platform.

text
NEXT_PUBLIC_MEDUSA_BACKEND_URL=https://api.yourdomain.com
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_...

Then add the storefront's domain to STORE_CORS and AUTH_CORS on the backend. Include Vercel preview domains if your team uses them, or previews will fail against the API in a way that looks like a code problem.

Costs

Roughly, for a mid-size store:

ServiceMonthly
Postgres$10–50
Redis$5–20
Server (1–2 replicas)$20–80
Worker$10–20
**Total****~$45–170**

Plus Vercel for the storefront. Comfortably cheaper than a hosted platform's plan fee at any meaningful revenue, and it does not scale with GMV.

When to move on

Railway is excellent up to a point. Consider moving when you need VPC-private database access, compliance boundaries your auditor wants documented, or fine-grained autoscaling. AWS deployment covers that path. Most stores never need it.

Preview environments

Railway can spin up an environment per pull request, which is genuinely useful for commerce work where reviewing a checkout change by reading a diff is not enough.

Two things to get right:

Do not point previews at production data. Use a separate database and seed it. A reviewer clicking through a checkout should not be able to place an order against the live catalog.

Add preview domains to CORS. Railway generates a hostname per environment, so STORE_CORS and AUTH_CORS need a wildcard-equivalent list for previews or every preview fails in a way that looks like a code bug.

Monitoring and logs

Railway's built-in logs are enough for a small store, and stop being enough at the point where you are grepping across two services.

  • Ship structured JSON logs to a real aggregator once you have more than one service. See observability.
  • Watch the metrics tab for memory. Node applications that leak show up as a sawtooth against the ceiling.
  • Alert on service restarts. Repeated restarts on the worker usually mean an unhandled rejection in a subscriber, which is otherwise invisible.
  • Keep the deploy history open during a release. Railway shows the pre-deploy command output, which is where a failed migration will appear.

When Railway bites

Two failure modes worth knowing before they happen. A pre-deploy command that fails leaves the previous version serving — which is correct behaviour and can be surprising if you assumed the deploy went out. And **the generated *.up.railway.app domain stays live after you attach a custom domain**, so make sure your CORS and MEDUSA_BACKEND_URL name the custom domain, or the dashboard will work on one hostname and not the other.

Scaling on Railway

Vertical first, horizontal second, which is the opposite of the usual advice and correct here because the database is the bottleneck.

Vertical. Increase the server service's memory and CPU. A single well-resourced instance handles a surprising amount of commerce traffic, and it avoids multiplying database connections.

Horizontal. Add replicas once one instance is genuinely saturated. Watch Postgres connections as you do — each replica brings its own pool, and Railway's Postgres has a connection ceiling you will meet sooner than a CPU one. See scaling Medusa.

Worker. Rarely needs more than one. If background work falls behind, partition queues rather than adding identical workers.

Set replica counts explicitly rather than relying on defaults, and revisit them before a peak season instead of during one.


We deploy Medusa on Railway for clients weekly. Ask for the setup.

Frequently asked questions

Can I deploy Medusa on Railway?

Yes, and it is one of the fastest correct setups available. You need four services: managed Postgres, managed Redis, a Medusa service in server mode and a second Medusa service from the same repository in worker mode.

How do I run Medusa migrations on Railway?

Put `npx medusa db:migrate` in the service's `preDeployCommand`. Railway runs it once per deploy before the new container receives traffic, which avoids the race you get when multiple replicas migrate at boot.

Why does my Railway build fail on Medusa?

Usually memory — the admin dashboard build needs around 2GB. Set `NODE_OPTIONS=--max-old-space-size=2048` on the service and rebuild.

Do I need two Railway services for Medusa?

Yes for production. One in server mode handles HTTP; one in worker mode runs scheduled jobs and subscribers. Without the split, every web replica also executes scheduled jobs.

How much does hosting Medusa on Railway cost?

Roughly $45–170 a month for a mid-size store across Postgres, Redis, server and worker, plus a storefront host. It scales with traffic rather than revenue.

Should the storefront also go on Railway?

Usually not. A Next.js storefront benefits from a CDN-first platform like Vercel, with its edge network and image optimisation. Keep the backend on Railway and the storefront on Vercel, and add the storefront domain to the backend's CORS settings.

[ Keep reading ]