Self-Hosting Medusa: E-Commerce Platform 2026
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):
-
Products → Add Product
- Title, description, images
- Variants (size, color)
- Pricing (multiple currencies)
- Inventory tracking
-
Collections → organize products into groups
-
Gift Cards → create gift card products
-
Discounts → percentage or fixed amount codes
Step 8: Configure Shipping and Tax
Shipping:
- Settings → Regions → add your regions
- Settings → Shipping → add shipping options per region
- Configure flat rate, free shipping, or calculated rates
Tax:
- Settings → Tax → configure tax rates per region
- 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 → Settings → Team.
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
| Products | RAM | CPU | Disk |
|---|---|---|---|
| 1-100 | 2 GB | 2 cores | 10 GB |
| 100-1K | 4 GB | 4 cores | 30 GB |
| 1K-10K | 8 GB | 8 cores | 50 GB |
VPS Recommendations
| Provider | Spec (500 products) | Price |
|---|---|---|
| Hetzner | 4 vCPU, 8 GB RAM | €8/month |
| DigitalOcean | 2 vCPU, 4 GB RAM | $24/month |
| Linode | 2 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 Settings → Regions. 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.