<!-- OSSAlt AI-readable guide source -->
<!-- Canonical: https://ossalt.com/guides/self-hosting-guide-medusa-2026 -->
<!-- Raw Markdown: https://ossalt.com/guides/self-hosting-guide-medusa-2026/raw.md -->
<!-- Source path: content/guides/self-hosting-guide-medusa-2026.mdx -->

---
og_image: "/images/guides/self-hosting-guide-medusa-2026.webp"
title: "Self-Hosting Medusa: E-Commerce Platform 2026"
description: "Deploy Medusa on your own VPS with Docker — Stripe payments, storefront, product management, and production tips for self-hosted headless commerce in 2026."
date: "2026-03-08"
author: "OSSAlt Team"
tags: ["medusa", "e-commerce", "self-hosting", "docker", "guide"]
tier: 1
---

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](/guides/self-hosting-vps-comparison-2026) 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

```bash
# 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

```yaml
# 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:
```bash
openssl rand -hex 32  # JWT_SECRET
openssl rand -hex 32  # COOKIE_SECRET
```

## Step 3: Start Medusa

```bash
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
}
```

```bash
sudo systemctl restart caddy
```

## Step 5: Set Up Payments

**Stripe (recommended):**
```bash
npm install medusa-payment-stripe
```

Configure in `medusa-config.js`:
```javascript
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:
```env
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):**
```bash
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:

```javascript
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. **Products** → **Add 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. **Settings** → **Regions** → add your regions
2. **Settings** → **Shipping** → add shipping options per region
3. Configure flat rate, free shipping, or calculated rates

**Tax:**
1. **Settings** → **Tax** → configure tax rates per region
2. Or integrate TaxJar for automatic calculation

**Email notifications:**
```bash
npm install medusa-plugin-sendgrid
# or
npm install medusa-plugin-resend
```

## Step 9: Set Up Admin Users

```bash
# 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):**
```bash
npm install medusa-file-s3
```

```javascript
// 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:**
```bash
# 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](/guides/automated-server-backups-restic-rclone-2026) to push encrypted copies to object storage. Losing an e-commerce database means losing order history, customer records, and inventory state.

**Updates:**
```bash
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](/guides/self-hosting-security-checklist-2026) 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.

```bash
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:**

```bash
sudo apt install fail2ban -y
```

`/etc/fail2ban/jail.local`:

```ini
[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.

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

**Disable SSH password auth:**

```
PasswordAuthentication no
PermitRootLogin no
```

Restart sshd: `sudo systemctl restart sshd`

**Automatic security updates:**

```bash
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](https://www.ossalt.com) — features, transaction fees, and flexibility side by side.*

*See open source alternatives to Medusa on [OSSAlt](https://www.ossalt.com/alternatives/medusa).*
