3 MIN READ

Deploying Medusa to Production: Architecture, Checklist and Pitfalls

What a production Medusa deployment actually needs — process separation, Redis, migrations, health checks — and the four mistakes that cause the first outage.

BY RAHUL MEHTAUPDATED
Illustration for “Deploying Medusa to Production: Architecture, Checklist and Pitfalls” — Deployment

Medusa runs locally in one command. Production is a different question, and the gap between the two is where most first deployments get into trouble — usually in the same four ways.

This is the architecture we deploy, and the checklist we run before pointing a domain at it.

The shape of a production deployment

Six pieces, at minimum:

ComponentPurposeNotes
Server processServes HTTP — Store API, Admin API, dashboardScale horizontally
Worker processRuns scheduled jobs and subscribersOne or more; never zero
PostgresEverythingManaged, with backups and PITR
RedisEvents, workflow engine, cache, sessionsManaged
File storageProduct images and uploadsS3 or compatible
CDNStorefront and assetsIn front of the storefront

The storefront is deployed separately, usually to Vercel — see the Next.js storefront guide.

Server and worker separation

This is the decision that matters most, and the one most often skipped.

By default a Medusa instance does everything: serves requests, runs subscribers, executes scheduled jobs. Run three of those instances and every scheduled job fires three times.

Medusa supports a worker mode for exactly this:

medusa-config.tsts
module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    redisUrl: process.env.REDIS_URL,
    workerMode: process.env.MEDUSA_WORKER_MODE as "server" | "worker" | "shared",
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET,
      cookieSecret: process.env.COOKIE_SECRET,
    },
  },
})

Deploy the same image twice with different values: MEDUSA_WORKER_MODE=server for the web-facing instances, MEDUSA_WORKER_MODE=worker for background processing. Disable the admin build on the worker — it does not serve it.

Get this wrong and the symptoms are duplicate emails, duplicated syncs and a scheduled job that runs once per instance. They look like application bugs and are not.

Redis, not memory

Three modules default to in-memory implementations that are fine locally and wrong in production:

medusa-config.tsts
modules: [
  {
    resolve: "@medusajs/medusa/event-bus-redis",
    options: { redisUrl: process.env.EVENTS_REDIS_URL },
  },
  {
    resolve: "@medusajs/medusa/workflow-engine-redis",
    options: { redis: { url: process.env.WE_REDIS_URL } },
  },
  {
    resolve: "@medusajs/medusa/cache-redis",
    options: { redisUrl: process.env.CACHE_REDIS_URL },
  },
]

Without these: events are lost on restart and do not reach other instances, long-running workflows cannot resume, and every instance keeps its own cache. Separate Redis databases per concern make it far easier to reason about a memory problem later.

Migrations on deploy

Run them as a distinct step, before the new code takes traffic:

bash
npx medusa db:migrate

Rules worth holding to:

  • Never run migrations from application boot. Three instances starting simultaneously will race.
  • Run them once per deploy, from a release step or a one-off task.
  • Keep them backwards compatible for one release. During a rolling deploy old and new code both run against the migrated schema. Add columns first, backfill, then drop in a later release.

More in Medusa database migrations.

Build considerations

medusa build compiles the backend and the admin dashboard. The admin build is the memory-hungry part; 512MB build containers routinely fail on it. Give the build step 2GB and stop debugging phantom failures.

Output goes to .medusa/server, which is what you run:

package.jsonjson
{
  "scripts": {
    "build": "medusa build",
    "start": "cd .medusa/server && medusa start"
  }
}

Health checks

Point your platform's health check at the framework's own endpoint:

text
GET /health

Configure a startup grace period. Medusa loads modules and verifies the database on boot, and an aggressive check will kill the container before it is ready — producing a crash loop that looks like an application failure.

The pre-launch checklist

text
[ ] Server and worker deployed as separate processes
[ ] Redis configured for event bus, workflow engine and cache
[ ] Postgres backups on, point-in-time recovery verified by an actual restore
[ ] Migrations run as a deploy step, not at boot
[ ] JWT_SECRET and COOKIE_SECRET set to real random values
[ ] CORS restricted to your real domains
[ ] File storage on S3 or equivalent, not local disk
[ ] Admin route not publicly discoverable, or IP restricted
[ ] Rate limiting in front of the auth endpoints
[ ] Error tracking and structured logs shipping somewhere
[ ] Health check with a startup grace period
[ ] A load test at 3× expected peak

The one people skip is the restore test. A backup you have never restored is a hypothesis.

The four mistakes

  1. No worker separation. Duplicate jobs, mysterious double-sends.
  2. In-memory event bus. Silent data loss on every deploy.
  3. Local file storage. Images vanish when the container recycles.
  4. Default secrets. JWT_SECRET=supersecret in production is an incident waiting to be written up.

Platform-specific walkthroughs: Docker, Railway and AWS. For traffic growth, scaling Medusa.

Security hardening

The items that come up in every security review, none of which are on by default:

Do not expose the admin on the same hostname as the API if you can avoid it, or at minimum put it behind IP restrictions or a VPN. A public /app is a public login form.

Rate limit the auth endpoints. /auth/*, password reset and cart creation are the three worth limiting at the edge. Without it, credential stuffing costs an attacker nothing.

Rotate the publishable key if it leaks into the wrong sales channel. It is not a secret, but it does scope what a storefront can see.

Restrict CORS to real origins, including preview domains explicitly. Wildcards on a credentialed API defeat the point of the same-origin policy.

Keep dependencies current. yarn npm audit in CI, and a scheduled dependency update PR. Commerce applications are attractive targets and the framework moves quickly.

Rollback

A deploy you cannot undo is a deploy you should not run. What "rollback" means depends on what shipped:

ShippedRollback
Application code onlyRedeploy the previous image
Additive migration (new nullable column)Redeploy previous image; leave the column
Destructive migration (dropped column)Restore from backup — this is why you avoid them
Config changeRevert the variable, redeploy

The practical rule is to keep migrations additive and backwards compatible for one release, which turns most rollbacks into "redeploy the previous image" instead of "restore the database". Migration patterns.


We deploy and operate Medusa for clients who would rather not. Ask about a managed setup.

Frequently asked questions

Where should I host Medusa?

Anywhere that runs a Node process with a managed Postgres and Redis. Railway and Render are the fastest to get right; AWS ECS or Fargate suits teams already there; Kubernetes works but is rarely justified below meaningful scale. Serverless platforms are a poor fit because Medusa expects a long-lived process.

What is worker mode in Medusa?

A configuration that determines whether an instance serves HTTP requests, runs background jobs and subscribers, or both. Production deployments run at least one instance in `server` mode and at least one in `worker` mode, so scaling the web tier does not multiply scheduled job execution.

Do I need Redis to run Medusa?

Not to boot, but yes for production. Without it the event bus is in-memory — events are lost on restart and never reach other instances — the workflow engine cannot resume long-running flows, and each instance caches separately.

How do I run Medusa migrations on deploy?

As a discrete release step running `npx medusa db:migrate` before the new version takes traffic. Never run migrations at application boot: concurrent instances will race, and a failed migration leaves you with a partially started fleet.

How much does it cost to host Medusa?

Roughly $100–500 a month for a mid-size store: application hosting, managed Postgres, managed Redis, object storage and a CDN. Costs scale with traffic rather than revenue, which is the main structural difference from a hosted platform.

Why does my Medusa build run out of memory?

Because `medusa build` also builds the admin dashboard, which is the memory-intensive part. Allocate at least 2GB to the build step. This is a build-time constraint only; the runtime container can be much smaller.

[ Keep reading ]