So you have decided to set up an online store and you would rather set it up your self, you’re in the right place.

We’ll Deploy a MedusaJS Backend and Storefront on an Ubuntu Server. In a later article we’ll look at using other AWS services to build a more resilient version. I’ll link it here when it’s ready [link]

  • Introduction
  • Pre-requisites
  • Step-by-Step Deployment

    • STEP 1) Server Provisioning
    • STEP 2) Installing Prerequisites
    • STEP 3) Running the Medusa Backend
    • STEP 4) Reverse Proxy with Nginx
    • STEP 5) Setting up the Frontend
    • STEP 6) Debugging and Fixes
    • STEP 7) Next Steps (Future Work)
  • Why Deploy MedusaJS on a Scalable Server?

There are many modern, headless commerce platforms, and MedusaJS is one of the most versatile for building full-featured online stores. Medusa makes store development seamless but deploying a production-ready application can be challenging without the right setup and automation practices.

That’s what this article will focus on, configuring an Ubuntu server to handle it and serving it through Nginx so safe to say that by the end we’ll have

A live MedusaJS application running on your server

Clearly separated and functional routes for admin, API, authentication, and storefront

A persistent frontend using PM2 for reliability

Nginx properly configured to handle the admin panel, country-specific routes and SSL

So whether you’re a developer looking to build more features for a specific use-case or just exploring headless commerce, this step-by-step guide will give you everything you need to host Medusa yourself.

Server Provisioning

Start by provisioning an ubuntu server, we’ll create a new security group with inbound rules; 22 from your IP, 80 and 443 from anywhere.
You should have something like this

A t3 medium server with about 30GB of storage space should do just fine.

Installing the Prerequisites

All we need is a Server, a domain and a stripe account.

We’ll need to install quite a few things to get our Medusa Store up and running.

  • Node

  • Docker & Docker Compose

  • Curl & Git

  • PM2

  • Nginx

  • Certbot

We can use this script to install everything we need.
We’ll install the Certbot later since it requires interaction

#!/bin/bash # Update system sudo apt update && sudo apt upgrade -y # Install prerequisites sudo apt install -y ca-certificates curl gnupg lsb-release # Add Docker’s official GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg |  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # Add Docker’s APT repository echo  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg]  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" |  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine, CLI, containerd, and Docker Compose plugin sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # Enable and start Docker sudo systemctl enable docker sudo systemctl start docker # Allow current user to run Docker without sudo sudo usermod -aG docker $USER newgrp docker # Verify Docker docker --version docker compose version # Install Git sudo apt install -y git # Install Nginx sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx # Install Node.js (LTS) and npm curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt install -y nodejs # Install PM2 globally sudo npm install -g pm2 # Setup PM2 startup (so it restarts apps on reboot) pm2 startup systemd -u $USER --hp $HOME 

Running the Backend Server

We’ll set up the Medusa Backend using Docker.

Let’s begin by cloning the Repo

git clone https://github.com/medusajs/medusa-starter-default.git --depth=1 medusa-server

Next, create a Docker compose file, docker-compose.yml in the new repo

services: # PostgreSQL Database postgres: image: postgres:15-alpine container_name: medusa_postgres restart: unless-stopped environment: POSTGRES_DB: medusa-store POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data networks: - medusa_network # Redis redis: image: redis:7-alpine container_name: medusa_redis restart: unless-stopped ports: - "6379:6379" networks: - medusa_network # Medusa Server # This service runs the Medusa backend application # and the admin dashboard. medusa: build: . container_name: medusa_backend restart: unless-stopped depends_on: - postgres - redis ports: - "9000:9000" environment: - NODE_ENV=production - DATABASE_URL=postgres://postgres:postgres@postgres:5432/medusa-store - REDIS_URL=redis://redis:6379 env_file: - .env volumes: - .:/app - /app/node_modules networks: - medusa_network volumes: postgres_data: networks: medusa_network: driver: bridge 

This creates three new services we’ll need, Redis, Postgres and Medusa

Create a Dockerfile, Dockerfile

# Development Dockerfile for Medusa FROM node:20-alpine # Set working directory WORKDIR /server # Copy package files and npm config COPY package.json package-lock.json ./ # Install all dependencies using npm RUN npm install # Copy source code COPY . . # Expose the port Medusa runs on EXPOSE 9000 # Start with migrations and then the development server CMD ["./start.sh"] 

Now create the start script start.sh and give it permission to run with chmod u+x start.sh

#!/bin/sh # Run migrations and start server echo "Running database migrations..." npx medusa db:migrate echo "Seeding database..." npm run seed || echo "Seeding failed, continuing..." echo "Starting Medusa development server..." npm run dev 

Run npm install to install the dependencies we’ll be working with.

Replace the medusa-config.ts file with this

import { loadEnv, defineConfig } from '@medusajs/framework/utils' loadEnv(process.env.NODE_ENV || 'production', process.cwd()) module.exports = defineConfig({ projectConfig: { databaseUrl: process.env.DATABASE_URL, databaseDriverOptions: { ssl: false, sslmode: "disable", }, http: { storeCors: process.env.STORE_CORS!, adminCors: process.env.ADMIN_CORS!, authCors: process.env.AUTH_CORS!, jwtSecret: process.env.JWT_SECRET || "supersecret", cookieSecret: process.env.COOKIE_SECRET || "supersecret", }, }, admin: { vite: () => { return { server: { allowedHosts: [".domainhere.com"], // add your domain here }, } }, }, }) 

Don’t forget to replace with your domain name where i indicated

In the package.json script section add this

{
"scripts": {
// Other scripts...
"docker:up": "docker compose up --build -d",
"docker:down": "docker compose down"
}
}

Create a .dockerignore file and add these to it

node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.git
.gitignore
README.md
.env.test
.nyc_output
coverage
.DS_Store
*.log
dist
build

Next create a .env file and add these, replacing with your domain where necessary

STORE_CORS=http://localhost:8000,https://docs.medusajs.com,https://domainhere.com,https://www.domainhere.com ADMIN_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com,https://domainhere.com,https://domainhere.com/app AUTH_CORS=http://localhost:5173,http://localhost:9000,https://docs.medusajs.com,https://domainhere.com,https://domainhere.com/app REDIS_URL=redis://redis:6379 JWT_SECRET=supersecret COOKIE_SECRET=supersecret DATABASE_URL=postgres://postgres:postgres@postgres:5432/medusa-store DB_NAME=medusa-v2 

Never expose your secrets but it should look like this

To start your application use npm run docker:up

To create a new admin user run

docker compose run --rm medusa npx medusa user -e admin@example.com -p supersecret

Replace with your email and password

To check your logs if everything runs smoothly
docker compose logs -f

Reverse Proxy with Nginx

Now let’s setup Nginx

We’ve installed Nginx previously so we should be seeing the “Welcome to Nginx page” on the server’s url

Create a Config file in /etc/nginx/sites-available and name it anything you want. I used the name of my domain, workrate.online

This config file sets up redirects to your Admin dashboard, frontend and API
Just replace my domain name, workrate.online with yours

server { listen 80; server_name workrate.online www.workrate.online; # --- Frontend (Next.js Storefront) --- location ~ ^/(dk|us|ng)(/.*)?$ { proxy_pass http://localhost:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } # Root frontend fallback location / { proxy_pass http://localhost:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } # --- Medusa Backend Routes --- location /store/ { proxy_pass http://localhost:9000/store/; } location /admin/ { proxy_pass http://localhost:9000/admin/; } location /auth/ { proxy_pass http://localhost:9000/auth/; } location /app/ { proxy_pass http://localhost:9000/app/; } } 

Enable the newly created config with
sudo ln -s /etc/nginx/sites-available/yourfilenamehere/etc/nginx/sites-enabled/

Remove the default config, it sometimes interfere
sudo rm /etc/nginx/sites-enabled/default

Test and reload your config

sudo nginx -t
sudo systemctl reload nginx

Don’t forget to point your domain to your server with your DNS provider.

Also you should set up Certbot to provide HTTPS by running

sudo apt install -y certbot python3-certbot-nginx
sudo certbot –nginx -d yourdomainname.com -d www.yourdomainname.com

You should now be able to access your Admin panel from your domain

Setting up the frontend

We’ll be setting up a GitHub Actions pipeline with the frontend because it seems more practical, the backend almost never changes but the frontend will require a lot of UI work and therefore constant pushing.

Anyways, let’s start by cloning the storefront

git clone https://github.com/medusajs/nextjs-starter-medusa medusa-storefront

Run npm install to install all dependencies

Create a .env file

#Your Medusa backend, should be updated to where you are hosting your server. Remember to update CORS settings for your server. See – https://docs.medusajs.com/learn/configurations/medusa-config#httpstorecors MEDUSA_BACKEND_URL=http://localhost:9000 # Your publishable key that can be attached to sales channels. See - https://docs.medusajs.com/resources/storefront-development/publishable-api-keys NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= # Your store URL, should be updated to where you are hosting your storefront. NEXT_PUBLIC_BASE_URL=http://localhost:8000 # Your preferred default region. When middleware cannot determine the user region from the "x-vercel-country" header, the default region will be used. ISO-2 lowercase format. NEXT_PUBLIC_DEFAULT_REGION=dk # Your Stripe public key. See – https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe NEXT_PUBLIC_STRIPE_KEY=pk_test_dQZtCPCXzFgnPf7YRp1ETSs7 # Your Next.js revalidation secret. See – https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#on-demand-revalidation REVALIDATE_SECRET=supersecret NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000 

You’ll need your Publishable key and Stripe Key {You can use the test one i attached for development purpose only}

You can use SSH tunnelling to access your port 9000 through your browser, login and get your Publishable Key. Run this on your local machine not the server

Or just go through your domain/app

ssh -i /path/to/your-key.pem -L 9000:localhost:9000 ubuntu@<your-ec2-public-ip>

You should then be able to go to http://localhost:9000/app/settings/publishable-api-keys to retrieve your own keys

Use npm run dev to make sure everything is running smoothly

GitHub Action

Generate new secret keys on your local machine with
ssh-keygen -t rsa -b 4096 -C "example@email.com"

Follow the prompt and manually append your generated .pub keys to ~/.ssh/authorized_keys

Create a GitHub repo, Medusa-storefront

Copy the private keys and go over to your Github Repo

Navigate to Settings -> Secrets & Variables -> Actions

Create a new repository secret named SSH_PRIVATE_KEY and enter the keys you copied earlier.

Remember the values from the .env files, we’ll create some secrets and variables so we can SSH into over server, build the frontend and deploy it every time it detects a push.

SSH_PRIVATE_KEY Secret
SERVER_HOST Secret Your Public IPv4 Address
SERVER_USER Secret ubuntu
APP_DIR Secret /var/www/storefront
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY Secret
NEXT_PUBLIC_STRIPE_KEY Secret
REVALIDATE_SECRET Secret
MEDUSA_BACKEND_URL Variable https://domainname
NEXT_PUBLIC_MEDUSA_BACKEND_URL Variable https://domainname
NEXT_PUBLIC_BASE_URL Variable https://domainname
NEXT_PUBLIC_DEFAULT_REGION Variable dk

Next in your server create a file in the store-front directory in .github/workflows/deploy.yml

Add this to the file save and push to your own local repo

name: Deploy Frontend on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest env: MEDUSA_BACKEND_URL: ${{ vars.MEDUSA_BACKEND_URL }} NEXT_PUBLIC_MEDUSA_BACKEND_URL: ${{ vars.NEXT_PUBLIC_MEDUSA_BACKEND_URL }} NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} NEXT_PUBLIC_DEFAULT_REGION: ${{ vars.NEXT_PUBLIC_DEFAULT_REGION }} NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY }} NEXT_PUBLIC_STRIPE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_KEY }} REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }} steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 20 - name: Create .env file run: | cat > .env << EOF MEDUSA_BACKEND_URL=${{ vars.MEDUSA_BACKEND_URL }} NEXT_PUBLIC_MEDUSA_BACKEND_URL=${{ vars.NEXT_PUBLIC_MEDUSA_BACKEND_URL }} NEXT_PUBLIC_BASE_URL=${{ vars.NEXT_PUBLIC_BASE_URL }} NEXT_PUBLIC_DEFAULT_REGION=${{ vars.NEXT_PUBLIC_DEFAULT_REGION }} NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY }} NEXT_PUBLIC_STRIPE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_KEY }} REVALIDATE_SECRET=${{ secrets.REVALIDATE_SECRET }} EOF - name: Install dependencies and build run: | npm install npm run build - name: Copy build files and .env to server uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} source: ".,!.git" target: "${{ secrets.APP_DIR }}" - name: Restart app uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd ${{ secrets.APP_DIR }} # Install production dependencies sudo npm install # Restart PM2 process sudo pm2 delete medusa-storefront || true sudo pm2 start npm --name medusa-"storefront" -- run start sudo pm2 save 

Use these commands to make sure the APP_DIR exists and has the correct permission to run


sudo mkdir -p /var/www/storefront
sudo chown -R ubuntu:ubuntu /var/www/storefront

Debugging, Fixes and Additional Configs

All you have left to do is make a change to the frontend, i overhauled the Storefront landing page. Make a push, let it build and test to make sure everything works fine.
Check it out here.
https://github.com/aregbesolaisrael/medusa-storefront

Next Steps

While this setup works for small and medium businesses, in my next article i’ll cover scaling Medusa with AWS services (ECS, RDS, CloudFront) and setting up monitoring and alerts.


Source: DEV Community.


Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.