Back to Blog
Tutorial12 min read2026-07-02

How to Deploy a Medusa.js E-commerce Backend to Production

Medusa needs PostgreSQL, Redis, and a worker process to run well in production. Here's how to deploy the backend correctly, including the server/worker split and the environment variables that trip people up.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Medusa is a strong open-source alternative to hosted commerce platforms, but it has more moving parts than a typical Node app. It expects PostgreSQL, Redis, and — in any serious setup — a separation between the API server and a background worker. Get those right and the rest is straightforward.

What Medusa actually needs

A production Medusa v2 deployment has these dependencies:

ComponentRoleRequired?
PostgreSQLPrimary data storeYes
RedisEvent bus, workflow engine, cacheStrongly recommended
Medusa serverServes the Store + Admin APIsYes
Medusa workerProcesses subscribers/scheduled jobsYes for production

The single biggest mistake is running everything in one process. Medusa supports a workerMode so the API server stays responsive while a separate process handles events, scheduled jobs, and long-running workflows.

Configure server vs worker mode

Medusa reads a MEDUSA_WORKER_MODE style switch through your config. In medusa-config.ts:

import { defineConfig, loadEnv } from '@medusajs/framework/utils';

loadEnv(process.env.NODE_ENV, process.cwd());

export default defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    redisUrl: process.env.REDIS_URL,
    workerMode: process.env.MEDUSA_WORKER_MODE as 'shared' | 'worker' | 'server',
    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
    }
  }
});

You then deploy two services from the same image:

  • Server: MEDUSA_WORKER_MODE=server, runs medusa start, serves HTTP.
  • Worker: MEDUSA_WORKER_MODE=worker, runs medusa start, no public HTTP.

Both share the same DATABASE_URL and REDIS_URL.

The environment variables that trip people up

CORS is the number one support question. Medusa has three separate CORS settings, and they are exact-match origin lists:

STORE_CORS=https://storefront.example.com
ADMIN_CORS=https://admin.example.com
AUTH_CORS=https://admin.example.com,https://storefront.example.com

If the admin dashboard shows network errors, it is almost always ADMIN_CORS or AUTH_CORS missing the exact origin (including scheme and no trailing slash).

Also set strong secrets — these sign sessions and tokens:

JWT_SECRET=$(openssl rand -base64 32)
COOKIE_SECRET=$(openssl rand -base64 32)

Build and migrate

Medusa builds to a .medusa/server directory. The production start sequence is:

# Build
npx medusa build

# Run migrations (once, before serving)
npx medusa db:migrate

# Start
cd .medusa/server && npm install && npx medusa start

Run db:migrate as a discrete step on deploy — not on every container boot, or two replicas will race each other.

A Dockerfile that works

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx medusa build
WORKDIR /app/.medusa/server
RUN npm install
EXPOSE 9000
CMD ["npx", "medusa", "start"]

The same image runs as both server and worker — only MEDUSA_WORKER_MODE differs.

Deploying on PandaStack

Medusa's architecture maps cleanly onto a platform that supports multiple services plus managed data stores:

  1. 1Provision managed PostgreSQL and Redis from the dashboard. PandaStack injects DATABASE_URL and a Redis URL automatically.
  2. 2Deploy the repo as a container app (the Dockerfile is auto-detected). Set MEDUSA_WORKER_MODE=server. This is your public API.
  3. 3Deploy the *same* repo as a second container app with MEDUSA_WORKER_MODE=worker and no public route.
  4. 4Run medusa db:migrate as a one-off before first traffic.
  5. 5Point a custom domain at the server; SSL is automatic.

Because both services pull the same image, a single push redeploys both consistently — and deploy history gives you a clean rollback if a migration misbehaves.

Operational notes

  • Admin user: create the first admin via npx medusa user -e admin@example.com -p supersecret against the production DB.
  • File storage: configure an S3-compatible provider for product images; local disk does not survive container restarts.
  • Scaling: the server scales horizontally behind a load balancer; keep exactly the right number of workers — duplicate scheduled jobs cause double-processing unless you understand Medusa's locking.
  • Redis is not optional in practice: without it, the event bus and workflow engine fall back to in-memory, which breaks across multiple replicas.

Verifying the deploy

# Health of the store API
curl -s https://api.example.com/health

# Admin auth endpoint should respond (401 without creds is fine)
curl -s -o /dev/null -w '%{http_code}' https://api.example.com/auth/user/emailpass

A 200 on /health plus a reachable admin login means the server, database, and Redis wiring are all correct.

References

  • [Medusa production deployment docs](https://docs.medusajs.com/learn/deployment)
  • [Medusa worker mode configuration](https://docs.medusajs.com/learn/production/worker-mode)
  • [Medusa medusa-config reference](https://docs.medusajs.com/resources/references/medusa-config)
  • [Medusa CLI reference](https://docs.medusajs.com/resources/medusa-cli)

---

Medusa's server + worker + PostgreSQL + Redis layout is exactly the kind of multi-service app PandaStack is built for, with managed databases auto-wired in. Spin one up free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also