Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Medusa Ubuntu
GUIDEInstall Guides

How to Install Medusa.js on Ubuntu 24.04 VPS: Self-Hosted Headless Commerce

23 min read

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?
  • Why Headless Commerce on Your Own VPS?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Create a Dedicated Medusa User
  • Step 3: Install Node.js 20 LTS via nvm
  • Step 4: Install PostgreSQL 16
  • Step 5: Install Redis
  • Step 6: Scaffold the Medusa Backend and Storefront
  • Step 7: Configure Environment Variables
  • Step 8: Run Migrations and Create an Admin User
  • Step 9: Start Medusa with pm2
  • Step 10: Configure Nginx as a Reverse Proxy
  • Step 11: Secure the Stack with TLS
  • Step 12: Configure Payment Providers and File Storage
  • Step 13: Set Up Regions, Currencies, and Sales Channels
  • Step 14: Deploy the Next.js Storefront
  • Backups and Upgrades
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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

    ScenarioShopify BasicShopify AdvancedShopify PlusMedusa on VPS
    Base monthly cost$39/mo$399/mo$2,300/moEUR 19.99/mo
    Transaction fee (third-party processor)2.0%0.5%0.15%0%
    Custom backend codeLimitedLimitedFunctions onlyUnlimited
    Storefront freedomLiquid themesLiquid themesHydrogen/LiquidAny framework
    B2B featuresAdd-onAdd-onIncludedIncluded
    Typical annual cost at 1,000 orders/mo ($50 AOV)$8,490$7,890$28,500EUR 240
    For any store doing more than a few hundred orders per month, the savings more than cover the VPS and a part-time developer.

    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.com and shop.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:

    bash
    ssh root@your-server-ip

    Step 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.

    bash
    sudo apt update && sudo apt upgrade -y

    Install a few build essentials that native Node modules (like sharp for image processing and argon2 for password hashing) require during installation:

    bash
    sudo apt install -y build-essential git curl ca-certificates gnupg ufw

    If your kernel was upgraded, reboot before continuing:

    bash
    sudo reboot

    Step 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.

    bash
    sudo adduser --disabled-password --gecos "" medusa
    sudo usermod -aG sudo medusa

    Switch to the new user for the rest of the install:

    bash
    sudo su - medusa

    All 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:

    bash
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

    Reload your shell configuration so the nvm command is available:

    bash
    source ~/.bashrc

    Install and activate Node.js 20 LTS:

    bash
    nvm install 20
    nvm alias default 20
    nvm use 20

    Verify the versions:

    bash
    node -v
    npm -v

    Expected output:

    text
    v20.18.1
    10.8.2

    Install 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.

    bash
    npm install -g pm2

    Step 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.

    bash
    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-16

    Start and enable the service:

    bash
    sudo systemctl enable --now postgresql
    sudo systemctl status postgresql

    Expected output:

    text
    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 ago

    Create a dedicated database and role for Medusa. Generate a strong password first:

    bash
    MEDUSA_DB_PASSWORD=$(openssl rand -base64 24)
    echo "Save this password: $MEDUSA_DB_PASSWORD"

    Create the role and database:

    bash
    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;
    SQL

    Verify the connection works:

    bash
    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.

    bash
    sudo apt install -y redis-server
    sudo systemctl enable --now redis-server

    Open /etc/redis/redis.conf and make sure supervised systemd is set so systemd can manage the process cleanly:

    bash
    sudo sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf
    sudo systemctl restart redis-server

    Verify Redis is responding:

    bash
    redis-cli ping

    Expected output:

    text
    PONG

    For 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:

    bash
    cd ~
    npx create-medusa-app@latest

    The installer will prompt you for:

  • Project name -- Enter medusa-store. This becomes the directory and the default database name.
  • Install Next.js storefront? -- Choose Yes. This creates a second directory (medusa-store-storefront) with the typed Store API client pre-wired.
  • Database credentials -- The scaffolder will detect the running PostgreSQL and prompt for host, user, password, and database. Use the values from Step 4.
  • 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:

    text
    /home/medusa/
        medusa-store/               # Backend + admin
        medusa-store-storefront/    # Next.js storefront

    Change into the backend:

    bash
    cd ~/medusa-store

    Step 7: Configure Environment Variables

    Medusa reads its configuration from .env in the backend directory. Generate strong secrets for JWT and cookies:

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

    bash
    nano ~/medusa-store/.env
    env
    # Database
    DATABASE_URL=postgres://medusa:[email protected]:5432/medusa_store

    Redis

    REDIS_URL=redis://127.0.0.1:6379

    Secrets (generated above)

    JWT_SECRET=REPLACE_WITH_JWT_SECRET COOKIE_SECRET=REPLACE_WITH_COOKIE_SECRET

    CORS (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:7001

    Admin backend URL (used by the embedded admin)

    MEDUSA_BACKEND_URL=https://api.yourstore.com

    Workers / mode

    MEDUSA_WORKER_MODE=shared

    Key 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 /auth endpoints introduced in v2. Include both store and admin origins.
    • MEDUSA_WORKER_MODE -- Use shared on a single-server setup so the worker runs inside the same Node process. For multi-server scale-out, run a separate process with MEDUSA_WORKER_MODE=worker.
    Save and close the file.

    Step 8: Run Migrations and Create an Admin User

    Apply the database schema. Medusa v2 exposes migrations through the medusa CLI:

    bash
    cd ~/medusa-store
    npx medusa db:migrate

    Expected output (abbreviated):

    text
    info: Running migrations for @medusajs/medusa...
    info: Running migrations for @medusajs/product...
    info: Running migrations for @medusajs/cart...
    ...
    info: Migrations completed successfully

    Seed the starter catalog (optional but useful for testing):

    bash
    npx medusa seed --seed-file=./src/scripts/seed.ts

    Create the first admin user. You will log in to the admin dashboard with these credentials:

    bash
    npx medusa user --email [email protected] --password 'ChangeMeImmediately!'

    Expected output:

    text
    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:

    bash
    npx medusa exec ./src/scripts/create-publishable-key.ts

    If 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:

    bash
    cd ~/medusa-store
    npm run build
    npm run start

    Medusa 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:

    javascript
    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:

    bash
    mkdir -p ~/logs
    pm2 start ~/medusa-store/ecosystem.config.js
    pm2 save

    Enable pm2 startup so the backend survives reboots:

    bash
    pm2 startup systemd -u medusa --hp /home/medusa

    pm2 prints a sudo command you need to paste and run once as root. After it finishes, confirm the backend is running:

    bash
    pm2 status
    pm2 logs medusa-backend --lines 40

    Alternative: systemd Unit

    If you prefer systemd over pm2, create /etc/systemd/system/medusa.service:

    ini
    [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:

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now medusa

    Step 10: Configure Nginx as a Reverse Proxy

    Install Nginx. See the Nginx installation guide for a standalone walk-through.

    bash
    sudo apt install -y nginx

    Create 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:

    bash
    sudo nano /etc/nginx/sites-available/medusa-api
    nginx
    upstream 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):

    bash
    sudo nano /etc/nginx/sites-available/medusa-store
    nginx
    upstream 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:

    bash
    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 nginx

    Open ports 80 and 443 in the firewall:

    bash
    sudo ufw allow OpenSSH
    sudo ufw allow 'Nginx Full'
    sudo ufw --force enable

    Step 11: Secure the Stack with TLS

    Install Certbot and issue certificates for both subdomains:

    bash
    sudo apt install -y certbot python3-certbot-nginx
    sudo certbot --nginx -d api.yourstore.com -d shop.yourstore.com

    Certbot writes the TLS config back into both Nginx site files and sets up a timer to renew every 60 days. Verify:

    bash
    sudo certbot renew --dry-run

    Visit 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:

    bash
    nano ~/medusa-store/medusa-config.js

    Stripe Payment Module

    Install the Stripe module:

    bash
    npm install @medusajs/payment-stripe

    Add it to the modules array in medusa-config.js:

    javascript
    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:

    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:

    bash
    npm install @medusajs/file-s3

    Add to modules:

    javascript
    {
      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:

    bash
    cd ~/medusa-store
    npm run build
    pm2 restart medusa-backend

    Step 13: Set Up Regions, Currencies, and Sales Channels

    Log into the admin at https://api.yourstore.com/app and complete the initial setup:

  • Settings > Regions -- Create regions for each country or group you sell to. A "United States" region uses USD, a "Eurozone" region uses EUR. Assign countries, tax rates, and payment/fulfillment providers per region.
  • Settings > Store > Currencies -- Enable every currency you priced products in. The default currency is the one the admin UI uses for totals.
  • Settings > Sales Channels -- A sales channel groups products available to a storefront. The default channel is created automatically. For multi-storefront setups (web store plus B2B portal plus mobile app), create one channel per storefront.
  • Settings > Publishable API Keys -- Create a key and associate it with your sales channel. Copy the pk_... value; the storefront needs it.
  • Products -- Add your first product with a variant, price, and inventory. Publish it to the sales channel from Step 3.
  • Step 14: Deploy the Next.js Storefront

    Change into the storefront project that was created alongside the backend:

    bash
    cd ~/medusa-store-storefront

    Edit the environment file:

    bash
    nano .env.local
    env
    NEXT_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=supersecret

    Build the production bundle:

    bash
    npm install
    npm run build

    Add the storefront to pm2 by extending ~/medusa-store/ecosystem.config.js:

    javascript
    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:

    bash
    pm2 restart ~/medusa-store/ecosystem.config.js
    pm2 save

    Visit 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:

    bash
    mkdir -p ~/backups
    nano ~/backup-medusa.sh
    bash
    #!/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 -delete

    Make it executable and schedule with cron:

    bash
    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

    bash
    PGPASSWORD='...' pg_restore \
      -h 127.0.0.1 -U medusa -d medusa_store \
      --clean --if-exists \
      /home/medusa/backups/medusa-YYYYMMDD-HHMMSS.dump

    Upgrading Medusa

    Medusa releases minor versions roughly every 2-4 weeks. To upgrade, bump the version in package.json, reinstall, run migrations, and restart:

    bash
    cd ~/medusa-store
    npm install @medusajs/medusa@latest @medusajs/admin@latest
    npx medusa db:migrate
    npm run build
    pm2 restart medusa-backend

    Always take a database backup before upgrading, and read the Medusa changelog for breaking changes between major versions.

    Troubleshooting

    ProblemCauseSolution
    Error: connect ECONNREFUSED 127.0.0.1:5432 at startupPostgreSQL not running or wrong portsudo systemctl status postgresql and verify DATABASE_URL host/port in .env.
    relation "..." does not exist on first requestMigrations never ran or partially failedRe-run npx medusa db:migrate. If it hangs, check PostgreSQL logs: sudo journalctl -u postgresql -n 100.
    Admin dashboard returns 401 Unauthorized after loginCOOKIE_SECRET changed or ADMIN_CORS does not include the admin originVerify 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' headerStorefront origin missing from STORE_CORS or AUTH_CORSAdd https://shop.yourstore.com to both vars (comma-separated, no trailing slash). Restart the backend.
    Error: Redis connection failedRedis not running or REDIS_URL wrongredis-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 KeyStripe secret key is wrong or still in test modeCheck STRIPE_API_KEY in .env. Live mode keys start with sk_live_, test keys with sk_test_. Restart backend after change.
    Storefront 502 Bad GatewayNext.js process died or pm2 never started itpm2 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 silentlyS3 credentials wrong or bucket CORS blocks uploadsTest 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 runningEvent bus or scheduler misconfiguredConfirm MEDUSA_WORKER_MODE=shared and that Redis is reachable. Check pm2 logs medusa-backend for worker startup messages.

    Viewing Logs

    bash
    pm2 logs medusa-backend --lines 100
    pm2 logs medusa-storefront --lines 100
    sudo journalctl -u postgresql -n 100
    sudo journalctl -u redis-server -n 100

    FAQ

    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, and max_connections for 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/health and 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.com subdomain at it. Catch breaking changes in plugins or schema migrations before they hit production.
    • Integrate an email service -- Install the @medusajs/notification-sendgrid or 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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket