Skip to main content

Self-Hosting Medusa: E-Commerce Platform 2026

·OSSAlt Team
medusae-commerceself-hostingdockerguide
Share:

Medusa is the open source Shopify alternative — a headless commerce platform with no transaction fees, full API access, and a modular architecture. Self-hosting means you own your store completely.

Why Self-Host Medusa

Shopify's pricing model has two components that compound as your store grows: the monthly subscription and transaction fees. The Basic plan at $39/month charges 2% transaction fees on every sale (waived if you use Shopify Payments, unavailable outside select countries). Shopify plan at $105/month drops fees to 1%. Advanced at $399/month takes 0.5%.

At $10,000/month in revenue using Shopify Basic: you're paying $39 subscription + $200 in transaction fees = $239/month, or $2,868/year. At $100K/month revenue on Shopify Advanced: $399 + $500 in fees = $899/month, $10,788/year — and that's before Shopify Plus territory.

Medusa on a €8/month Hetzner server with Stripe's standard 2.9% + $0.30 pricing costs $96/year in hosting. At $10K/month revenue: $96 + ~$290 Stripe fees = $386/year. You keep the difference — roughly $2,500/year saved at $10K revenue, much more at higher volumes.

True headless commerce. Medusa's API-first architecture means your storefront can be built with any technology — Next.js, Nuxt, React Native, Flutter. You're not constrained by Shopify's Liquid templating or theme limitations. Product pages, checkout flows, and cart logic are yours to build exactly as needed.

No feature gating. Multi-currency support, gift cards, discount rules, return management, and inventory tracking are all included in the open source core. Shopify gates many of these behind higher tiers or third-party apps (often $10-30/month each).

When NOT to self-host Medusa: Medusa requires genuine engineering capacity to maintain. Setting up payments, running database migrations after updates, and debugging issues when something breaks requires developer involvement. If you're a non-technical merchant, Shopify's managed infrastructure and one-click app ecosystem are worth the cost. Medusa also has a less mature ecosystem of third-party integrations compared to Shopify's 7,000+ apps.

Prerequisites

Running a self-hosted e-commerce backend is a serious infrastructure commitment — your store's revenue depends on uptime. Choose your VPS provider with reliability and support in mind.

Server specs: 2 GB RAM handles a small catalog with low traffic. For a store serving real customers, 4 GB RAM provides headroom for database queries, the Node.js API process, and Redis. CPU matters during product import and search indexing — 2 vCPU handles most catalog sizes.

Operating system: Ubuntu 22.04 LTS. Medusa is a Node.js application; the Docker setup works cleanly on Ubuntu 22.04 and the community resources universally target it.

PostgreSQL and Redis: Medusa requires both. PostgreSQL stores all catalog, order, customer, and inventory data. Redis handles caching, queued jobs, and session management. Both are included in the Docker Compose setup.

Payment provider credentials: You'll need Stripe API keys (or another supported provider) before your store can process real transactions. Get Stripe API keys from dashboard.stripe.com and configure webhook endpoints — Stripe sends payment events to Medusa via webhook, which is how order confirmation emails and inventory updates trigger.

Domain strategy: Consider separating the admin dashboard domain from the storefront. admin.yourdomain.com for the Medusa backend/admin UI, store.yourdomain.com for the customer-facing Next.js storefront. This makes security configuration cleaner — you can restrict admin access by IP while keeping the store fully public.

Skills required: Intermediate Node.js and Docker knowledge. You should understand npm package installation, environment variable configuration, and basic PostgreSQL operations. Building and deploying a storefront (Step 6) requires Next.js familiarity.

Requirements

  • VPS with 2 GB RAM minimum (4 GB recommended)
  • Node.js 20+ or Docker
  • Domain name (e.g., store.yourdomain.com)
  • PostgreSQL database
  • Redis
  • 20+ GB disk

Step 1: Create Medusa Project

# Create new Medusa project
npx create-medusa-app@latest my-store
cd my-store

# Or clone for Docker setup
git clone https://github.com/medusajs/medusa.git
cd medusa

Step 2: Docker Compose Setup

# docker-compose.yml
services:
  medusa:
    build: .
    container_name: medusa
    restart: unless-stopped
    ports:
      - "9000:9000"
    environment:
      - DATABASE_URL=postgresql://medusa:your-strong-password@db:5432/medusa
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=your-jwt-secret
      - COOKIE_SECRET=your-cookie-secret
      - STORE_CORS=https://store.yourdomain.com
      - ADMIN_CORS=https://admin.yourdomain.com
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    container_name: medusa-db
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=medusa
      - POSTGRES_USER=medusa
      - POSTGRES_PASSWORD=your-strong-password

  redis:
    image: redis:7-alpine
    container_name: medusa-redis
    restart: unless-stopped
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Generate secrets:

openssl rand -hex 32  # JWT_SECRET
openssl rand -hex 32  # COOKIE_SECRET

Step 3: Start Medusa

docker compose up -d

# Run migrations and seed
docker exec medusa npx medusa db:migrate
docker exec medusa npx medusa seed --seed-file=data/seed.json

Admin dashboard at http://localhost:9000/app API at http://localhost:9000

Step 4: Reverse Proxy (Caddy)

# /etc/caddy/Caddyfile

# API and Admin
admin.yourdomain.com {
    reverse_proxy localhost:9000
}

# Storefront (Step 6)
store.yourdomain.com {
    reverse_proxy localhost:3000
}
sudo systemctl restart caddy

Step 5: Set Up Payments

Stripe (recommended):

npm install medusa-payment-stripe

Configure in medusa-config.js:

module.exports = {
  plugins: [{
    resolve: 'medusa-payment-stripe',
    options: {
      api_key: process.env.STRIPE_API_KEY,
      webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
    },
  }],
}

Set environment variables:

STRIPE_API_KEY=sk_live_your_key
STRIPE_WEBHOOK_SECRET=whsec_your_secret

No transaction fees from Medusa — only Stripe's standard 2.9% + $0.30.

Step 6: Deploy Storefront

Next.js Starter (recommended):

npx create-medusa-app@latest --with-nextjs-starter
cd my-store-storefront

# Configure
echo "NEXT_PUBLIC_MEDUSA_BACKEND_URL=https://admin.yourdomain.com" > .env.local

# Build and start
npm run build
npm start

Or build a custom storefront with any framework using the Medusa JS SDK:

import Medusa from '@medusajs/js-sdk'

const medusa = new Medusa({ baseUrl: 'https://admin.yourdomain.com' })

// List products
const { products } = await medusa.store.products.list()

// Get product details
const { product } = await medusa.store.products.retrieve('prod_123')

// Create cart
const { cart } = await medusa.store.carts.create({})

// Add item to cart
await medusa.store.carts.lineItems.create(cart.id, {
  variant_id: 'variant_123',
  quantity: 1,
})

// Complete checkout
const { order } = await medusa.store.carts.complete(cart.id)

Step 7: Configure Products

In the admin dashboard (admin.yourdomain.com/app):

  1. ProductsAdd Product

    • Title, description, images
    • Variants (size, color)
    • Pricing (multiple currencies)
    • Inventory tracking
  2. Collections → organize products into groups

  3. Gift Cards → create gift card products

  4. Discounts → percentage or fixed amount codes

Step 8: Configure Shipping and Tax

Shipping:

  1. SettingsRegions → add your regions
  2. SettingsShipping → add shipping options per region
  3. Configure flat rate, free shipping, or calculated rates

Tax:

  1. SettingsTax → configure tax rates per region
  2. Or integrate TaxJar for automatic calculation

Email notifications:

npm install medusa-plugin-sendgrid
# or
npm install medusa-plugin-resend

Step 9: Set Up Admin Users

# Create admin user via CLI
docker exec medusa npx medusa user --email admin@yourdomain.com --password your-password

Or invite via Admin Dashboard → SettingsTeam.

Production Hardening

File storage (S3 for product images):

npm install medusa-file-s3
// medusa-config.js
plugins: [{
  resolve: 'medusa-file-s3',
  options: {
    s3_url: 'https://s3.yourdomain.com',
    bucket: 'medusa-uploads',
    region: 'us-east-1',
    access_key_id: process.env.S3_ACCESS_KEY,
    secret_access_key: process.env.S3_SECRET_KEY,
  },
}],

Backups:

# Database backup (daily cron)
docker exec medusa-db pg_dump -U medusa medusa > /backups/medusa-$(date +%Y%m%d).sql

# File storage backup (if using local)
tar czf /backups/medusa-uploads-$(date +%Y%m%d).tar.gz ./uploads

Your order database is your business's financial record. Combine local pg_dump backups with automated server backups using restic to push encrypted copies to object storage. Losing an e-commerce database means losing order history, customer records, and inventory state.

Updates:

npm install @medusajs/medusa@latest
npx medusa db:migrate
docker compose restart medusa

Monitoring:

  • Monitor API endpoint (port 9000)
  • Monitor storefront (port 3000)
  • Track order completion rates
  • Set up alerts for failed payments

Resource Usage

ProductsRAMCPUDisk
1-1002 GB2 cores10 GB
100-1K4 GB4 cores30 GB
1K-10K8 GB8 cores50 GB

VPS Recommendations

ProviderSpec (500 products)Price
Hetzner4 vCPU, 8 GB RAM€8/month
DigitalOcean2 vCPU, 4 GB RAM$24/month
Linode2 vCPU, 4 GB RAM$24/month

vs Shopify Basic ($39/month + 2.9% fees): At $10K/month revenue, Shopify costs $639/month. Medusa on Hetzner costs $8/month + Stripe's 2.9%.

Production Security Hardening

An e-commerce backend is a high-value target — it contains customer PII, payment tokens, and order history. Beyond application security, server hardening is non-negotiable. See the self-hosting security checklist for a comprehensive baseline, then apply these Medusa-specific steps.

UFW firewall: The Medusa API and storefront should only be accessible through Caddy. Block direct container access.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 9000/tcp   # Block direct Medusa API access
sudo ufw deny 3000/tcp   # Block direct storefront access
sudo ufw enable

Fail2ban:

sudo apt install fail2ban -y

/etc/fail2ban/jail.local:

[sshd]
enabled = true
maxretry = 5
bantime = 3600
findtime = 600

Secrets in environment files: JWT_SECRET, COOKIE_SECRET, and STRIPE_API_KEY must never be hardcoded in Docker Compose files or committed to version control.

chmod 600 .env
echo ".env" >> .gitignore

Disable SSH password auth:

PasswordAuthentication no
PermitRootLogin no

Restart sshd: sudo systemctl restart sshd

Automatic security updates:

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Admin dashboard restriction: The Medusa admin at admin.yourdomain.com/app should be IP-restricted if your admin team works from known locations. Use Caddy's remote_ip matcher to block all traffic except your office and VPN IP ranges. A compromised admin panel means full access to customer data and order history.

Stripe webhook security: Verify every Stripe webhook payload using the webhook signature (STRIPE_WEBHOOK_SECRET). The Medusa Stripe plugin does this automatically when you configure the webhook secret. Never process payment events without signature verification.

Troubleshooting Common Issues

"Cannot find module" errors on startup

This happens when the Docker image was built without running npm install first, or when a custom plugin was added without rebuilding the image. Rebuild: docker compose build --no-cache medusa && docker compose up -d medusa.

Migrations fail with "column already exists" or "table not found"

Medusa's migrations are idempotent but can fail if the database is in an inconsistent state (e.g., a failed partial migration). Check migration status: docker exec medusa npx medusa migrations show. If migrations are stuck, consult the Medusa migration troubleshooting guide — do not manually edit the migration state table without understanding the implications.

Stripe webhooks not triggering order updates

Stripe webhooks require a publicly accessible HTTPS endpoint. Verify the webhook endpoint URL in your Stripe dashboard matches your Medusa installation (e.g., https://admin.yourdomain.com/hooks/payment/stripe). Test with Stripe's webhook testing tool. Also confirm the STRIPE_WEBHOOK_SECRET matches the signing secret shown in your Stripe webhook configuration.

CORS errors when storefront calls the API

The STORE_CORS and ADMIN_CORS environment variables must exactly match the origins making requests. If your storefront is at https://store.yourdomain.com, set STORE_CORS=https://store.yourdomain.com (no trailing slash). Multiple origins are comma-separated. After changing CORS settings, restart the Medusa container.

Product images not displaying after upload

If using local file storage, product images are stored inside the container. If the container is removed and recreated (during an update, for example), images are lost unless the storage directory is mounted as a volume. Switch to S3-compatible storage before going to production — it's the only reliable approach for file persistence across container lifecycles.

Ongoing Maintenance and Operations

Running a self-hosted e-commerce platform is a serious operational commitment. Unlike internal tools, your store is revenue-generating infrastructure — every minute of downtime costs sales.

Release cadence and updates. Medusa's core team releases updates frequently. Subscribe to the Medusa changelog on GitHub to track security patches and breaking changes. For an e-commerce store, test updates in a staging environment before deploying to production. A broken checkout flow discovered at 2am on a weekend is significantly worse than a brief delay in applying a patch.

Order management workflow. The Medusa admin panel is the hub for daily operations: processing new orders, managing fulfillment status, issuing refunds, and handling returns. Define internal processes for each order state — who receives new order notifications, who updates fulfillment status, who processes returns. Integrate Medusa's webhook events with Slack or your communication tool so the right person gets notified when an order requires action.

Inventory tracking. Medusa's inventory module tracks stock levels per product variant. Configure low-stock alerts by creating a workflow (via n8n or a custom webhook consumer) that fires when inventory drops below a threshold. Medusa sends an inventory-item.updated event via webhooks when stock changes — subscribe to this event to trigger automated reorder notifications.

Multi-currency and international commerce. Medusa supports selling in multiple currencies and regions natively. Configure separate pricing per region in SettingsRegions. For European customers, configure VAT rates per country. Medusa's tax calculation is automatic once regions are configured, but verify tax rates against your local tax authority's requirements — tax compliance is your responsibility with a self-hosted store.

Storefront performance. The Next.js storefront's performance directly impacts conversion rates. Deploy the storefront with proper caching headers for static assets. Use Cloudflare as a CDN in front of your server — it handles DDoS mitigation, caches static pages, and reduces origin server load significantly. The Medusa backend API responses should be cached at the application level (Redis) for product listings and category pages that don't change frequently.

Database backups before every update. E-commerce databases contain order history, which may be required for tax and accounting purposes. Before every Medusa update (not just major versions), run a database backup. A broken migration that requires a rollback means your database backup is your only recovery option. Test your restore procedure quarterly: take a backup, spin up a temporary database container, restore it, and verify a few recent orders are present.

Scaling for traffic spikes. Product launches and sales events can spike traffic 10-100x above normal levels. Hetzner allows server resizing (increasing CPU/RAM) in minutes from the console. Before a major sale, scale up the server the morning of the event and scale back down afterward. The few dollars of extra cost is worth it compared to a degraded checkout experience during peak demand.


Compare e-commerce platforms on OSSAlt — features, transaction fees, and flexibility side by side.

See open source alternatives to Medusa on OSSAlt.

The SaaS-to-Self-Hosted Migration Guide (Free PDF)

Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.

Join 300+ self-hosters. Unsubscribe in one click.