
Photo by Luke Chesser on Unsplash
Deploying Next.js + Medusa v2 to Production: A Complete Guide
Deploying Next.js + Medusa v2 to Production: A Complete Guide
Medusa v2 starters ship with a complete local development stack — backend, admin dashboard, storefront, and seed data — but production deployment is something every team has to design themselves. This guide walks through the decisions and concrete steps for getting a Next.js + Medusa v2 stack running in production in a single afternoon.
This isn't a "happy path tutorial." It covers what breaks, what costs money, and how to run the backend without an incident every week.
TL;DR
- You need three services: Medusa backend (Node.js), PostgreSQL, Redis. Plus storage, CDN, email, and monitoring.
- Easiest path: Railway or Render for the backend, their managed Postgres + Redis, S3 or R2 for storage, Resend for email. ~$80-150/month.
- More control: Fly.io or a VPS + managed database. ~$50-120/month.
- Enterprise path: AWS or GCP with RDS + ElastiCache + ALB. ~$200-500+/month.
- The hardest parts are database migrations, secret management, and Stripe webhook testing.
- Budget for ops: 4-8 hours/month of routine maintenance.
Browse Medusa starter templates as your starting point.
Architecture at a glance
+----------------+ +------------------+ +----------------+
| Next.js | | Medusa Backend | | Shopify / ... |
| Storefront |------>| (Node.js API) |<----->| Stripe |
| (Vercel) | | (Railway/Render)| | (payments) |
+----------------+ +------------------+ +----------------+
| |
| +----> PostgreSQL (managed)
| |
| +----> Redis (managed)
| |
| +----> S3 / R2 (images)
v |
+----------------+ +----> Resend / Postmark (email)
| Customers |
| (browser / |
| mobile app) |
+----------------+
Four deployable units:
- Next.js storefront — static-friendly, deploys to any Node or edge platform
- Medusa backend API — long-running Node.js process, needs persistent storage
- Medusa admin dashboard — usually runs alongside the backend on the same domain
- Worker (if you use background jobs) — same codebase as the backend, different process
Plus the managed dependencies (Postgres, Redis, storage, email, monitoring).
Step 1: Pick your hosting platform
There's no objectively "best" hosting for a Medusa stack. Pick based on how much ops work you want to do.
Option A — Railway or Render (easiest)
Good for: Solo founders and small teams. You want one dashboard, one bill, no AWS complexity.
What you get:
- Managed deployments from a Git repo
- Managed PostgreSQL and Redis add-ons
- Automatic HTTPS and custom domains
- Built-in logs and metrics
- One-click rollbacks
Cost: $60-120/month for a realistic setup (backend service + Postgres + Redis + bandwidth).
Trade-offs: Less flexibility than raw cloud. You can't easily change database versions, tune Postgres config, or run custom network rules. You're trusting the platform.
Setup:
# Create a new Railway project
railway init
# Link your Medusa backend repo
railway link
# Add PostgreSQL and Redis services
railway add postgresql redis
# Set environment variables from .env.production
railway variables set DATABASE_URL=... REDIS_URL=... STORE_CORS=https://mystore.com
Railway auto-detects Medusa's package.json scripts and runs medusa start in production.
Option B — Fly.io or a VPS (middle ground)
Good for: Teams with DevOps capacity who want more control without jumping to AWS.
What you get:
- Global deployment (Fly.io) or single-region VPS (DigitalOcean, Hetzner)
- Custom Dockerfile deploys
- SSH access to the runtime
- More control over database tuning
Cost: $40-100/month for a VPS + managed Postgres. Fly.io runs similar for a small-to-medium stack.
Trade-offs: You're responsible for more of the operational surface. Backups, monitoring alerts, and security patches need your attention.
Option C — AWS / GCP (enterprise)
Good for: Teams already on AWS, or stores that need regional compliance, private networking, or high-volume scaling.
What you get:
- RDS PostgreSQL (with point-in-time recovery)
- ElastiCache Redis
- ECS or App Runner for the Node.js service
- Full VPC control and IAM scoping
- Route 53 + CloudFront for DNS and CDN
Cost: $200-500+/month for a realistic production setup, before data transfer.
Trade-offs: Most operational overhead, most complexity. Also the most mature if you're already running AWS workloads.
Step 2: Set up the database
PostgreSQL is the single biggest operational decision. Don't self-host it unless you really know what you're doing.
Managed PostgreSQL options
| Provider | Good for | Cost (entry) | Notes |
|---|---|---|---|
| Railway Postgres | Simplicity | $5-20/month | Same dashboard as app |
| Render Postgres | Simplicity | $7-25/month | Same dashboard as app |
| Neon | Serverless Postgres | $0-19/month free tier | Branching useful for staging |
| Supabase | If you also use their auth | $25+/month | Full postgres + extras |
| AWS RDS | Enterprise | $30-300+/month | Maximum control |
| Fly.io Postgres | Global | $10-50/month | Region-aware |
For a production Medusa store, you need at least:
- PostgreSQL 15 or 16 (Medusa v2 supports both)
- 10 GB storage minimum, growing with your order volume
- Daily automated backups with point-in-time recovery
- Connection pooling — Medusa opens lots of connections; use PgBouncer if your provider doesn't pool automatically
Running migrations in production
This is where most deployments get scary. Medusa ships its own migration runner:
# In production, after every deploy
pnpm medusa db:migrate
Rule: migrations run before the new code starts, not after. A Dockerfile CMD wrapper works:
CMD ["sh", "-c", "pnpm medusa db:migrate && pnpm medusa start"]
Railway and Render have "pre-deploy" hooks that do this cleanly. On Fly.io, use a release command:
# fly.toml
[deploy]
release_command = "pnpm medusa db:migrate"
Never let two backend instances try to run migrations at the same time. Migrations need to be serialized. The release-command pattern guarantees this.
Step 3: Set up Redis
Medusa v2 uses Redis for the event bus, cache, and (optionally) workflow state.
Managed Redis options
| Provider | Good for | Cost |
|---|---|---|
| Railway Redis | Matches your Postgres | $5-15/month |
| Render Redis | Matches your Postgres | $10-25/month |
| Upstash Redis | Serverless, pay-per-request | $0-20/month typical |
| AWS ElastiCache | Enterprise | $15-100+/month |
For a Medusa stack, 256 MB is enough for stores under ~10k orders/month. Scale up when you start seeing memory pressure in Medusa logs.
Important: Medusa assumes Redis is always up. A transient Redis outage can cause request failures. Pick a provider with good uptime SLAs, and don't run Redis on the same tiny VPS as your database.
Step 4: Set up file storage
Product images need to live somewhere. Medusa supports S3-compatible storage out of the box.
Options, from cheapest to fanciest
- Cloudflare R2 — S3-compatible, zero egress fees, $0.015/GB/month. Best default for most stores.
- AWS S3 — The original. Egress fees can bite you.
- Backblaze B2 — Cheap, S3-compatible, decent free tier.
- Vercel Blob — Integrated with Vercel deployments, premium pricing.
Configure in your Medusa medusa-config.ts:
export default defineConfig({
projectConfig: {
// ...
},
modules: [
{
resolve: "@medusajs/medusa/file",
options: {
providers: [
{
resolve: "@medusajs/medusa/file-s3",
id: "s3",
options: {
file_url: process.env.S3_FILE_URL,
access_key_id: process.env.S3_ACCESS_KEY_ID,
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
region: process.env.S3_REGION,
bucket: process.env.S3_BUCKET,
endpoint: process.env.S3_ENDPOINT, // R2 or custom
},
},
],
},
},
],
})
Images are served directly from the CDN in front of your storage bucket. The storefront gets the image URLs from the Medusa API and renders them with next/image or a direct <img> tag.
Step 5: Configure Stripe
Stripe is the default payment provider for MVPHub Medusa templates. The setup is:
- Create a Stripe account (or use an existing one)
- Enable the payment methods you want (card, Apple Pay, Google Pay, etc.)
- Copy your secret key from the Stripe dashboard
- Set env vars in your Medusa backend:
STRIPE_API_KEY=sk_live_... STRIPE_WEBHOOK_SECRET=whsec_... - Register a webhook endpoint in Stripe pointing at your Medusa backend:
https://api.yourstore.com/hooks/payment/stripe - Copy the webhook signing secret from Stripe back into
STRIPE_WEBHOOK_SECRET
Test it by using Stripe's test mode first. Create a test payment, watch it show up in Medusa's admin dashboard, and verify the order transitions through the expected states.
See the Stripe checkout integration guide for the full checkout flow and webhook patterns.
Step 6: Deploy the Next.js storefront
The storefront is the easy part. Deploy it to Vercel, Cloudflare Pages, or any Next.js-compatible host.
Environment variables
The storefront needs three critical env vars:
NEXT_PUBLIC_MEDUSA_BACKEND_URL=https://api.yourstore.com
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_...
NEXT_PUBLIC_DEFAULT_REGION=us
The publishable key is created in the Medusa admin under "Settings → Publishable API Keys." It scopes the storefront's access to the backend.
Vercel deployment
vercel --prod
Vercel detects Next.js, runs pnpm build, and deploys. Set the env vars in the Vercel project settings. Set the root directory to apps/storefront if you're in a monorepo.
Connecting to the backend
In production, the storefront hits your backend over HTTPS. CORS has to be configured correctly:
// apps/backend/medusa-config.ts
export default defineConfig({
projectConfig: {
http: {
storeCors: process.env.STORE_CORS ?? "https://mystore.com",
adminCors: process.env.ADMIN_CORS ?? "https://admin.mystore.com",
authCors: process.env.AUTH_CORS ?? "https://mystore.com",
},
},
})
If you get "CORS error" in browser console, 99% of the time STORE_CORS is wrong.
Step 7: Set up email
Order confirmations, password resets, and other transactional emails need to go out reliably.
Options:
- Resend — $0-20/month typical, clean DX
- Postmark — $15-50/month, enterprise-grade deliverability
- AWS SES — Cheapest at scale, lots of setup
- SendGrid — Middle ground
Configure Medusa's notification provider in medusa-config.ts:
{
resolve: "@medusajs/medusa/notification",
options: {
providers: [
{
resolve: "./src/modules/resend", // Custom Resend provider
id: "resend",
options: {
api_key: process.env.RESEND_API_KEY,
from_email: "orders@yourstore.com",
},
},
],
},
}
Test deliverability early. An order confirmation that lands in spam is almost worse than one that doesn't send at all.
Step 8: Monitoring and alerting
Minimum viable monitoring:
- Uptime checks — Pingdom, Better Uptime, or UptimeRobot pinging your
/healthendpoint every minute - Error tracking — Sentry on both backend and storefront
- Log aggregation — Whatever your host gives you (Railway logs, Vercel logs) is usually enough to start
- Database metrics — Your managed Postgres provider's built-in dashboard
Alerts to set up on day one:
- Backend down for > 2 minutes
- Error rate > 1% of requests
- Database CPU > 80% sustained
- Disk usage > 80%
- Stripe webhook delivery failures
Don't over-engineer monitoring at launch. The goal is to know when something is broken, not to build a Datadog dashboard nobody reads.
Step 9: Operational runbook
Before you flip DNS to production, write down the answers to these:
- How do I restart the backend? (Platform-specific command)
- How do I roll back a bad deploy? (Git revert + redeploy, or platform-native rollback)
- How do I restore the database from a backup? (Documented, tested, ideally practiced once)
- How do I invalidate the CDN cache? (If using CloudFront/Cloudflare)
- How do I trigger a manual Stripe webhook retry? (Stripe dashboard → Developers → Webhooks)
- Who gets paged for a P1? (Even if it's just you at 3am, write it down)
- What's the communication plan if Stripe goes down? (Status page, customer email, refund policy)
This runbook lives in the repo as docs/runbook.md or similar. Update it every time you hit a new failure mode.
Common deployment mistakes
Forgetting to run migrations before starting the new version. The service comes up, hits a schema it doesn't understand, and crashes. Use a release command.
Using the same Postgres database for staging and production. Obvious when said out loud, embarrassingly common. Use a separate database (or a separate Postgres service) for staging.
Exposing the admin dashboard to the public internet without IP allowlisting. The admin login is strong, but you don't need to hand attackers a target. Put it behind a VPN, IP allowlist, or at least a WAF rule.
Running the storefront and backend on the same domain without proper CORS. You'll get cookies and CORS errors you don't understand. Use subdomains: store.com for the storefront, api.store.com for the backend, admin.store.com for the admin dashboard.
Skipping the smoke test. After every deploy, load the homepage, add a product to cart, and complete a test checkout. Automate this if you can; do it manually if you can't.
Ongoing maintenance
A production Medusa stack needs about 4-8 hours of routine work per month. In order of frequency:
- Weekly: review error tracker, check uptime, verify backups actually ran
- Monthly: apply security patches (Node.js, npm packages, OS), review database size and query performance
- Quarterly: upgrade Medusa minor versions, audit env var drift between environments, test database restore
- Yearly: major version upgrades (Node.js LTS, Postgres, Medusa v3 when it lands)
This is less work than most teams expect but more than zero. Budget for it explicitly instead of assuming "it'll be fine."
Next steps
- Browse Medusa starter templates — production-grade starting points
- Medusa v2 Starter Guide — deeper architecture walkthrough
- Stripe Checkout integration guide — payment setup in detail
- Headless architecture pillar guide — broader context
- Compatibility matrix — deployment target support for Medusa templates
- Performance benchmarks — CWV data for Medusa-backed storefronts
Production deployment isn't glamorous, but it's the gap between "demo that works on my laptop" and "store that customers can actually buy from." Walk through this checklist once, and the second store you launch will take a third the time.







