How to Install Medusa.js on Ubuntu 24.04 VPS: Self-Hosted Headless Commerce
Medusa.js is the open-source, TypeScript-first commerce platform that lets you build custom e-commerce experiences without Shopify's monthly fees, transaction cuts, or template restrictions. This guide walks you through installing Medusa.js v2 on an Ubuntu 24.04 VPS from scratch, including PostgreSQL, Redis, the Medusa backend, the admin dashboard, and a production-ready Next.js storefront.
Why self-host? A Shopify Plus plan starts at $2,300/month. A properly sized VPS running Medusa.js costs EUR 19.99/month and gives you complete control over the code, database, and customer data. By the end of this guide you will have a full headless commerce stack serving real traffic.
Table of Contents
What is Medusa.js?
Medusa.js is an open-source headless commerce platform built on Node.js and TypeScript. It ships as a set of composable modules: product catalog, cart, order, fulfillment, payments, promotions, customers, inventory, and tax. Each module exposes a REST and JavaScript API, and the whole system is designed to be extended with custom modules, workflows, and subscribers rather than patched through themes or third-party apps.
Unlike Shopify, WooCommerce, or BigCommerce, Medusa does not ship with a frontend. You are expected to bring your own storefront, typically built with Next.js, Remix, Astro, or a mobile framework. Medusa provides a production-grade Next.js starter that communicates with the backend over a typed Store API, plus a React-based admin dashboard that runs as part of the same Node process.
Medusa v2 (released late 2024) introduced a new module-based architecture, a workflow engine modeled on temporal.io, native support for multi-region and multi-currency stores, and a reworked publishable API key system for sales channels. The entire codebase is MIT-licensed and available on GitHub.
Typical use cases include:
- Shopify alternatives for brands that want to own their stack end-to-end without per-order transaction fees
- B2B commerce with custom pricing, quote workflows, and net-payment terms that would require expensive Shopify Plus plans
- Marketplaces where multiple vendors share a storefront but need isolated inventory, payouts, and sales channels
- Subscription commerce and digital product delivery where custom fulfillment logic lives alongside the cart
- Omnichannel retail where the same backend powers a web store, a mobile app, a POS, and a B2B portal
Why Headless Commerce on Your Own VPS?
Running Medusa on your own server instead of a hosted SaaS platform has concrete advantages:
- No transaction fees -- Shopify charges 0.5% to 2% on every order on top of the monthly plan cost. Medusa has no such fee. You keep 100% of the margin minus what your payment processor charges.
- Complete data ownership -- Customer records, order history, and analytics live in your PostgreSQL database. There is no vendor lock-in and no risk of a platform suspending your store.
- API-first architecture -- The same backend can serve a web storefront, a native mobile app, a kiosk, and a B2B procurement portal without code duplication.
- TypeScript codebase you can read -- Every line of Medusa is open source. You can patch bugs, audit security, and extend behavior without waiting for a vendor release.
- Flat-rate hosting cost -- A Medusa store handling 10,000 orders per month costs the same VPS bill as one handling 100 orders. You scale vertically or horizontally when you need to.
- GDPR and PCI compliance on your terms -- When sensitive data lives on a server in a region you control, compliance documentation is straightforward.
- Plays nicely with your existing stack -- Drop Medusa behind Nginx, terminate TLS with Let's Encrypt, monitor it with Prometheus, and back it up with
pg_dump. No proprietary tooling required.
Cost Comparison: Medusa VPS vs. Hosted Platforms
| Scenario | Shopify Basic | Shopify Advanced | Shopify Plus | Medusa on VPS |
|---|---|---|---|---|
| Base monthly cost | $39/mo | $399/mo | $2,300/mo | EUR 19.99/mo |
| Transaction fee (third-party processor) | 2.0% | 0.5% | 0.15% | 0% |
| Custom backend code | Limited | Limited | Functions only | Unlimited |
| Storefront freedom | Liquid themes | Liquid themes | Hydrogen/Liquid | Any framework |
| B2B features | Add-on | Add-on | Included | Included |
| Typical annual cost at 1,000 orders/mo ($50 AOV) | $8,490 | $7,890 | $28,500 | EUR 240 |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- At least 4 GB of RAM (8 GB+ recommended for running backend, admin, storefront, PostgreSQL and Redis on one box)
- At least 40 GB of SSD storage for the OS, Node modules, database, and uploads
- A domain name with DNS pointing to your VPS (for example,
api.yourstore.comandshop.yourstore.com)
Recommended Plan: CloudCore Professional>
Medusa's backend, admin dashboard, Next.js storefront, PostgreSQL, and Redis all run comfortably on the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This gives you enough headroom for the full stack plus traffic bursts. For stores doing more than 10,000 orders per month, consider splitting the database onto its own VPS.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
Start by refreshing the package index and upgrading installed packages. This avoids dependency resolution failures when you install Node.js, PostgreSQL, and Redis.
sudo apt update && sudo apt upgrade -yInstall a few build essentials that native Node modules (like sharp for image processing and argon2 for password hashing) require during installation:
sudo apt install -y build-essential git curl ca-certificates gnupg ufwIf your kernel was upgraded, reboot before continuing:
sudo rebootStep 2: Create a Dedicated Medusa User
Running Medusa as root is a bad idea. Create a non-privileged medusa user that owns the application files and the pm2 process tree.
sudo adduser --disabled-password --gecos "" medusa
sudo usermod -aG sudo medusaSwitch to the new user for the rest of the install:
sudo su - medusaAll subsequent npm, npx, and medusa commands in this guide should be run as the medusa user unless a sudo prefix is shown.
Step 3: Install Node.js 20 LTS via nvm
Medusa v2 targets Node.js 20 LTS. Installing via nvm (Node Version Manager) is the most reliable approach because it lets you switch versions without reinstalling the OS package and avoids the stale Node binaries in the Ubuntu apt repos.
For a deeper walk-through of the Node.js install, see our guide to installing Node.js on Ubuntu 24.04.
Install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bashReload your shell configuration so the nvm command is available:
source ~/.bashrcInstall and activate Node.js 20 LTS:
nvm install 20
nvm alias default 20
nvm use 20Verify the versions:
node -v
npm -vExpected output:
v20.18.1
10.8.2Install pm2 globally. pm2 is the process manager that keeps the Medusa backend and the Next.js storefront alive across reboots and automatically restarts them on crashes.
npm install -g pm2Step 4: Install PostgreSQL 16
Medusa stores all commerce data (products, orders, customers, carts, inventory) in PostgreSQL. Version 16 is recommended for v2.
Add the official PostgreSQL APT repository so you get the latest 16.x release rather than the older default in Ubuntu's repos. For a full deep-dive, see our PostgreSQL installation guide.
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg
sudo apt update
sudo apt install -y postgresql-16 postgresql-contrib-16Start and enable the service:
sudo systemctl enable --now postgresql
sudo systemctl status postgresqlExpected output:
postgresql.service - PostgreSQL RDBMS
Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; preset: enabled)
Active: active (exited) since Wed 2026-04-16 10:00:00 UTC; 10s agoCreate a dedicated database and role for Medusa. Generate a strong password first:
MEDUSA_DB_PASSWORD=$(openssl rand -base64 24)
echo "Save this password: $MEDUSA_DB_PASSWORD"Create the role and database:
sudo -u postgres psql <<SQL
CREATE ROLE medusa WITH LOGIN PASSWORD '${MEDUSA_DB_PASSWORD}';
CREATE DATABASE medusa_store OWNER medusa;
GRANT ALL PRIVILEGES ON DATABASE medusa_store TO medusa;
SQLVerify the connection works:
PGPASSWORD="${MEDUSA_DB_PASSWORD}" psql -h 127.0.0.1 -U medusa -d medusa_store -c '\conninfo'Step 5: Install Redis
Medusa uses Redis for the workflow engine, event bus, cache, and background job queue. In production you want Redis, not the in-memory fallback.
For a standalone deep-dive see our Redis installation guide.
sudo apt install -y redis-server
sudo systemctl enable --now redis-serverOpen /etc/redis/redis.conf and make sure supervised systemd is set so systemd can manage the process cleanly:
sudo sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf
sudo systemctl restart redis-serverVerify Redis is responding:
redis-cli pingExpected output:
PONGFor a single-server setup, Redis binds to 127.0.0.1 by default, which is what we want. Do not expose Redis to the public internet.
Step 6: Scaffold the Medusa Backend and Storefront
Medusa ships an interactive scaffolder that creates both the backend project and a paired Next.js storefront. Run it as the medusa user from the home directory:
cd ~
npx create-medusa-app@latestThe installer will prompt you for:
medusa-store. This becomes the directory and the default database name.Yes. This creates a second directory (medusa-store-storefront) with the typed Store API client pre-wired.If the default password prompt does not accept your credentials, skip the automatic database setup and paste the values into .env manually after scaffolding.
When complete you will have two directories:
/home/medusa/
medusa-store/ # Backend + admin
medusa-store-storefront/ # Next.js storefrontChange into the backend:
cd ~/medusa-storeStep 7: Configure Environment Variables
Medusa reads its configuration from .env in the backend directory. Generate strong secrets for JWT and cookies:
JWT_SECRET=$(openssl rand -base64 48)
COOKIE_SECRET=$(openssl rand -base64 48)
echo "JWT_SECRET=$JWT_SECRET"
echo "COOKIE_SECRET=$COOKIE_SECRET"Open .env and set these values:
nano ~/medusa-store/.env# Database
DATABASE_URL=postgres://medusa:[email protected]:5432/medusa_storeRedis
REDIS_URL=redis://127.0.0.1:6379Secrets (generated above)
JWT_SECRET=REPLACE_WITH_JWT_SECRET
COOKIE_SECRET=REPLACE_WITH_COOKIE_SECRETCORS (comma-separated, no trailing slash)
STORE_CORS=https://shop.yourstore.com,http://localhost:8000
ADMIN_CORS=https://api.yourstore.com,http://localhost:7001
AUTH_CORS=https://shop.yourstore.com,https://api.yourstore.com,http://localhost:8000,http://localhost:7001Admin backend URL (used by the embedded admin)
MEDUSA_BACKEND_URL=https://api.yourstore.comWorkers / mode
MEDUSA_WORKER_MODE=sharedKey points:
STORE_CORS-- Origins allowed to hit the Store API (/store/*). This must include your storefront domain.ADMIN_CORS-- Origins allowed to hit the Admin API (/admin/*). Typically the domain where the embedded admin dashboard is served.AUTH_CORS-- Origins allowed to hit the unified/authendpoints introduced in v2. Include both store and admin origins.MEDUSA_WORKER_MODE-- Usesharedon a single-server setup so the worker runs inside the same Node process. For multi-server scale-out, run a separate process withMEDUSA_WORKER_MODE=worker.
Step 8: Run Migrations and Create an Admin User
Apply the database schema. Medusa v2 exposes migrations through the medusa CLI:
cd ~/medusa-store
npx medusa db:migrateExpected output (abbreviated):
info: Running migrations for @medusajs/medusa...
info: Running migrations for @medusajs/product...
info: Running migrations for @medusajs/cart...
...
info: Migrations completed successfullySeed the starter catalog (optional but useful for testing):
npx medusa seed --seed-file=./src/scripts/seed.tsCreate the first admin user. You will log in to the admin dashboard with these credentials:
npx medusa user --email [email protected] --password 'ChangeMeImmediately!'Expected output:
info: User created successfully.Generate a publishable API key for the storefront. The storefront needs this key to call the Store API on behalf of a specific sales channel:
npx medusa exec ./src/scripts/create-publishable-key.tsIf your scaffold did not include this script, you can create the key later through the admin UI at Settings > Publishable API Keys.
Step 9: Start Medusa with pm2
A quick sanity test first:
cd ~/medusa-store
npm run build
npm run startMedusa should boot in about 10-30 seconds. When you see Server is ready on port: 9000, hit Ctrl+C and switch over to pm2.
Create a pm2 ecosystem file at ~/medusa-store/ecosystem.config.js:
module.exports = {
apps: [
{
name: "medusa-backend",
cwd: "/home/medusa/medusa-store",
script: "npm",
args: "run start",
env: {
NODE_ENV: "production",
PORT: 9000,
},
max_memory_restart: "1G",
error_file: "/home/medusa/logs/medusa-error.log",
out_file: "/home/medusa/logs/medusa-out.log",
time: true,
},
],
};Create the log directory and start:
mkdir -p ~/logs
pm2 start ~/medusa-store/ecosystem.config.js
pm2 saveEnable pm2 startup so the backend survives reboots:
pm2 startup systemd -u medusa --hp /home/medusapm2 prints a sudo command you need to paste and run once as root. After it finishes, confirm the backend is running:
pm2 status
pm2 logs medusa-backend --lines 40Alternative: systemd Unit
If you prefer systemd over pm2, create /etc/systemd/system/medusa.service:
[Unit] Description=Medusa.js Backend After=network.target postgresql.service redis-server.service[Service] Type=simple User=medusa WorkingDirectory=/home/medusa/medusa-store ExecStart=/home/medusa/.nvm/versions/node/v20.18.1/bin/npm run start Restart=always RestartSec=5 Environment=NODE_ENV=production Environment=PORT=9000
[Install] WantedBy=multi-user.target
Then:
sudo systemctl daemon-reload
sudo systemctl enable --now medusaStep 10: Configure Nginx as a Reverse Proxy
Install Nginx. See the Nginx installation guide for a standalone walk-through.
sudo apt install -y nginxCreate an Nginx site for the Medusa backend and embedded admin. The admin dashboard is served by the same Node process at /app, so a single reverse proxy config handles both:
sudo nano /etc/nginx/sites-available/medusa-apiupstream medusa_backend { server 127.0.0.1:9000; keepalive 64; }server { listen 80; server_name api.yourstore.com;
# Let's Encrypt verification location /.well-known/acme-challenge/ { root /var/www/html; }
# Everything else goes to Medusa location / { proxy_pass http://medusa_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
client_max_body_size 25m; # Allow product image uploads proxy_read_timeout 600s; proxy_send_timeout 600s; } }
Create a second site for the storefront (we will deploy the Next.js app to port 8000 in Step 14):
sudo nano /etc/nginx/sites-available/medusa-storeupstream medusa_storefront { server 127.0.0.1:8000; keepalive 64; }server { listen 80; server_name shop.yourstore.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { proxy_pass http://medusa_storefront; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
Enable both sites and reload:
sudo ln -s /etc/nginx/sites-available/medusa-api /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/medusa-store /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxOpen ports 80 and 443 in the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enableStep 11: Secure the Stack with TLS
Install Certbot and issue certificates for both subdomains:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.yourstore.com -d shop.yourstore.comCertbot writes the TLS config back into both Nginx site files and sets up a timer to renew every 60 days. Verify:
sudo certbot renew --dry-runVisit https://api.yourstore.com/health in a browser. You should see {"status":"ok"}. Then visit https://api.yourstore.com/app -- the Medusa admin login screen should load. Sign in with the admin credentials from Step 8.
Step 12: Configure Payment Providers and File Storage
Medusa v2 uses a module-based system. Payment and file storage are configured in medusa-config.js at the root of the backend project.
Open the config file:
nano ~/medusa-store/medusa-config.jsStripe Payment Module
Install the Stripe module:
npm install @medusajs/payment-stripeAdd it to the modules array in medusa-config.js:
modules: [
// ... existing modules
{
resolve: "@medusajs/payment",
options: {
providers: [
{
resolve: "@medusajs/payment-stripe",
id: "stripe",
options: {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
},
},
],
},
},
],Add the keys to .env:
STRIPE_API_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...Point the Stripe webhook at https://api.yourstore.com/hooks/payment/stripe_stripe in the Stripe dashboard.
S3 File Storage (Uploads)
Install the S3 module:
npm install @medusajs/file-s3Add to modules:
{
resolve: "@medusajs/file",
options: {
providers: [
{
resolve: "@medusajs/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,
},
},
],
},
},Any S3-compatible object store works -- AWS S3, Cloudflare R2, Backblaze B2, or a self-hosted MinIO. Without this, product image uploads land on local disk, which does not scale past a single server.
Rebuild and restart after each config change:
cd ~/medusa-store
npm run build
pm2 restart medusa-backendStep 13: Set Up Regions, Currencies, and Sales Channels
Log into the admin at https://api.yourstore.com/app and complete the initial setup:
pk_... value; the storefront needs it.Step 14: Deploy the Next.js Storefront
Change into the storefront project that was created alongside the backend:
cd ~/medusa-store-storefrontEdit the environment file:
nano .env.localNEXT_PUBLIC_MEDUSA_BACKEND_URL=https://api.yourstore.com
NEXT_PUBLIC_BASE_URL=https://shop.yourstore.com
NEXT_PUBLIC_DEFAULT_REGION=us
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_REPLACE_WITH_KEY_FROM_ADMIN
REVALIDATE_SECRET=supersecretBuild the production bundle:
npm install
npm run buildAdd the storefront to pm2 by extending ~/medusa-store/ecosystem.config.js:
module.exports = {
apps: [
{
name: "medusa-backend",
cwd: "/home/medusa/medusa-store",
script: "npm",
args: "run start",
env: { NODE_ENV: "production", PORT: 9000 },
max_memory_restart: "1G",
error_file: "/home/medusa/logs/medusa-error.log",
out_file: "/home/medusa/logs/medusa-out.log",
time: true,
},
{
name: "medusa-storefront",
cwd: "/home/medusa/medusa-store-storefront",
script: "npm",
args: "run start",
env: { NODE_ENV: "production", PORT: 8000 },
max_memory_restart: "1G",
error_file: "/home/medusa/logs/storefront-error.log",
out_file: "/home/medusa/logs/storefront-out.log",
time: true,
},
],
};Reload pm2:
pm2 restart ~/medusa-store/ecosystem.config.js
pm2 saveVisit https://shop.yourstore.com. You should see the Next.js storefront with the product you seeded or created in Step 13.
Backups and Upgrades
Daily PostgreSQL Backup
Create a simple nightly backup script. As the medusa user:
mkdir -p ~/backups
nano ~/backup-medusa.sh#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/home/medusa/backups"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
PGPASSWORD='REPLACE_WITH_DB_PASSWORD' pg_dump \
-h 127.0.0.1 -U medusa -d medusa_store \
--format=custom \
--file="${BACKUP_DIR}/medusa-${TIMESTAMP}.dump"Keep last 14 days
find "${BACKUP_DIR}" -name "medusa-*.dump" -mtime +14 -deleteMake it executable and schedule with cron:
chmod +x ~/backup-medusa.sh
(crontab -l 2>/dev/null; echo "30 3 * /home/medusa/backup-medusa.sh >> /home/medusa/logs/backup.log 2>&1") | crontab -For off-site storage, rsync the backups/ directory to S3, Backblaze B2, or a second VPS.
Restoring from a Backup
PGPASSWORD='...' pg_restore \
-h 127.0.0.1 -U medusa -d medusa_store \
--clean --if-exists \
/home/medusa/backups/medusa-YYYYMMDD-HHMMSS.dumpUpgrading Medusa
Medusa releases minor versions roughly every 2-4 weeks. To upgrade, bump the version in package.json, reinstall, run migrations, and restart:
cd ~/medusa-store
npm install @medusajs/medusa@latest @medusajs/admin@latest
npx medusa db:migrate
npm run build
pm2 restart medusa-backendAlways take a database backup before upgrading, and read the Medusa changelog for breaking changes between major versions.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Error: connect ECONNREFUSED 127.0.0.1:5432 at startup | PostgreSQL not running or wrong port | sudo systemctl status postgresql and verify DATABASE_URL host/port in .env. |
relation "..." does not exist on first request | Migrations never ran or partially failed | Re-run npx medusa db:migrate. If it hangs, check PostgreSQL logs: sudo journalctl -u postgresql -n 100. |
Admin dashboard returns 401 Unauthorized after login | COOKIE_SECRET changed or ADMIN_CORS does not include the admin origin | Verify ADMIN_CORS and AUTH_CORS in .env. Clear browser cookies for the admin domain. Keep COOKIE_SECRET stable across restarts. |
Storefront fetch fails with CORS error: no 'Access-Control-Allow-Origin' header | Storefront origin missing from STORE_CORS or AUTH_CORS | Add https://shop.yourstore.com to both vars (comma-separated, no trailing slash). Restart the backend. |
Error: Redis connection failed | Redis not running or REDIS_URL wrong | redis-cli ping should return PONG. Verify REDIS_URL=redis://127.0.0.1:6379. Restart Redis: sudo systemctl restart redis-server. |
payment intent failed: Invalid API Key | Stripe secret key is wrong or still in test mode | Check STRIPE_API_KEY in .env. Live mode keys start with sk_live_, test keys with sk_test_. Restart backend after change. |
| Storefront 502 Bad Gateway | Next.js process died or pm2 never started it | pm2 status. If the storefront is errored, check pm2 logs medusa-storefront. Usually a missing env var or bad NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY. |
| Product image upload fails silently | S3 credentials wrong or bucket CORS blocks uploads | Test the S3 credentials with aws s3 ls s3://your-bucket. Set the bucket CORS policy to allow PUT from https://api.yourstore.com. |
Worker mode "shared" but no jobs running | Event bus or scheduler misconfigured | Confirm MEDUSA_WORKER_MODE=shared and that Redis is reachable. Check pm2 logs medusa-backend for worker startup messages. |
Viewing Logs
pm2 logs medusa-backend --lines 100
pm2 logs medusa-storefront --lines 100
sudo journalctl -u postgresql -n 100
sudo journalctl -u redis-server -n 100FAQ
How does Medusa.js compare to Shopify for a mid-market store?
Shopify is a full SaaS platform with hosting, theme store, app marketplace, and card processing bundled in. It is the right call when you want to spend zero developer hours and are comfortable paying 2-3% of revenue for that convenience. Medusa is the opposite: it is a codebase. You host it, extend it, and build the storefront yourself. For stores doing more than a few hundred orders a month with at least one developer on staff, Medusa is typically 10-20x cheaper annually and gives you unlimited flexibility. The break-even point is usually around $50K annual GMV.
Do I need separate servers for the backend, storefront, and database?
Not for small to medium stores. The CloudCore Professional plan (6 vCPU, 12 GB RAM) comfortably runs the Medusa backend, Next.js storefront, PostgreSQL, and Redis on the same box up to a few thousand orders per month. Above that, the usual next step is to move PostgreSQL to a dedicated managed database (or a second VPS), then split the storefront onto its own server so you can scale web traffic horizontally. The admin dashboard is embedded in the backend and does not need its own process.
Can I run Medusa v1 and Medusa v2 on the same server?
Technically yes, on different ports with separate PostgreSQL databases, but it is not recommended. v2 is a substantial rewrite with a new module system, a different admin UI, and different CLI. Plugins written for v1 do not run on v2 without porting. If you have a v1 store, plan a proper migration project rather than a side-by-side deployment. The official migration guide covers data export, schema diffs, and plugin replacements.
How do I add a custom module or business logic?
Medusa v2's module system is the extension point. Create a directory under src/modules/my-feature/, export a Module with its service and data models, and register it in medusa-config.js. Workflows (long-running, resumable business processes) live in src/workflows/ and subscribers (event handlers) in src/subscribers/. All three are TypeScript-first with typed inputs and outputs. The Medusa docs module guide has end-to-end examples.
What about SEO for the storefront?
The official Next.js starter ships with App Router, server components, and generateMetadata hooks. Product pages, category pages, and CMS content all render server-side with full control over <title>, <meta description>, Open Graph tags, and JSON-LD. You are responsible for sitemaps, structured data, and page speed -- Medusa does not hide these behind a platform abstraction the way Shopify does. On the flip side, you can implement programmatic SEO, custom canonical logic, and international routing without fighting a theme.
Next Steps
Now that Medusa is live on your VPS, here are recommended follow-ups:
- Harden PostgreSQL for production -- Tune
shared_buffers,work_mem, andmax_connectionsfor your RAM. Enable point-in-time recovery with WAL archiving. See the PostgreSQL install guide for tuning tips.
- Put Cloudflare in front of the storefront -- Free tier gives you DDoS protection, edge caching for static assets, and a global CDN. Keep the backend subdomain on "DNS only" (grey cloud) unless you have a plan to handle cookie-sensitive proxying.
- Install Uptime Kuma for monitoring -- Monitor both
api.yourstore.com/healthand the storefront home page. Alert to Slack, Discord, or email on downtime or slow responses.
- Add a staging environment -- Clone the VPS, restore a recent backup, and point a
staging-api.yourstore.comsubdomain at it. Catch breaking changes in plugins or schema migrations before they hit production.
- Integrate an email service -- Install the
@medusajs/notification-sendgridor Resend module and wire up order confirmation, shipping, and abandoned cart emails. Medusa emits the events already; you just need to subscribe and render templates.
- Browse the plugin ecosystem -- The Medusa docs integrations page lists community modules for MeiliSearch, Algolia, Segment, Klaviyo, and more. Each one is a drop-in module you register in
medusa-config.js.
Run Medusa.js on Infrastructure Built for Commerce>
Our CloudCore Professional plan is sized exactly for the Medusa + PostgreSQL + Redis + Next.js stack described in this guide. Deploy in minutes with full root access, NVMe storage, and unmetered bandwidth.>
- 6 vCPU cores for parallel request handling
- 12 GB RAM for Node, PostgreSQL shared buffers, and Redis
- 100 GB NVMe SSD for the database and product images
- Unmetered bandwidth for storefront traffic
- Ubuntu 24.04 LTS, ready for apt install
>
Launch Your CloudCore VPS -- EUR 19.99/month, cancel anytime.