How to Set Up a CI/CD Pipeline for Your Medusa.js Store with GitHub Actions
Shipping code manually to production is a gamble. One missed environment variable, one untested route, and your store goes down at the worst possible moment. For teams building on Medusa.js, automating the deploy process with GitHub Actions is not just a convenience. It is a foundation for shipping reliably, confidently, and fast.
This guide walks you through setting up a full CI/CD pipeline for your Medusa.js store using GitHub Actions. Whether you are running a solo project or coordinating across a team, the workflow patterns here will save you hours and reduce deployment risk significantly.
Why CI/CD Matters for Medusa.js Projects
Medusa.js is modular and highly configurable. That flexibility is its greatest strength, but it also means more moving parts during deployment. A custom module, a new payment integration, or a storefront update can introduce regressions that only show up in production.
A CI/CD pipeline gives your team a structured path from code commit to live production. Every push gets tested, validated, and deployed through a consistent process. You can read about Medusa's modular architecture in depth at the official Medusa.js documentation to understand why deployment consistency matters as your customisations grow.
Ready to automate your Medusa.js deployments?
Lets TalkWhat You Need Before You Begin
Before writing a single workflow file, get the following in place:
A Medusa.js v2 project pushed to a GitHub repository
A deployment target (Railway, Render, Fly.io, or a VPS with SSH access)
Environment variables stored in GitHub Secrets
Node.js 20 or above as your runtime
A PostgreSQL database and Redis instance accessible from your deployment environment
Understanding the Medusa.js Deploy Process
Deploying a Medusa.js store involves more than copying files to a server. You need to run database migrations, seed any required data, build the admin dashboard, and restart the Node.js process. GitHub Actions lets you automate each of these steps in a defined order.
Setting Up Your GitHub Actions Workflow File
Create a directory at .github/workflows/ in your repository root. Inside it, add a file named deploy.yml. This is where your entire CI/CD logic lives.
A clean starting structure for your workflow file should define three things: the trigger events, the environment, and the job steps. For most Medusa.js projects, you will want the pipeline to run on every push to your main branch and on every pull request to catch regressions before they merge.
Defining Triggers
Your workflow should trigger on pushes to main for deployment and on pull requests for testing. This separation keeps your production deploys deliberate while still running tests automatically on every code review. Using the on: push condition scoped to the main branch prevents feature branches from accidentally triggering production deploys.
Configuring the Environment
GitHub Actions runs your jobs on hosted runners. For a Medusa.js project, ubuntu-latest works well. Specify your Node.js version explicitly using the actions/setup-node action to avoid version drift between local development and CI. Locking to Node 20 matches Medusa v2's recommended runtime.
Need a custom CI/CD setup for Medusa.js?
Lets TalkStoring Secrets and Environment Variables
Never hardcode database credentials, JWT secrets, or API keys in your workflow file. GitHub provides an encrypted Secrets store accessible from Settings > Secrets and variables > Actions.
For a Medusa.js project, the minimum secrets you need are your database connection string, Redis URL, JWT secret, and cookie secret. You reference these in your workflow as context expressions, and GitHub injects the values at runtime. Your secrets never appear in logs or workflow outputs.
Writing the Test Job
Before anything reaches production, your pipeline should validate the codebase. The test job should install dependencies with npm ci, set up a test database using PostgreSQL as a service container within GitHub Actions, and run your test suite.
GitHub Actions supports service containers for PostgreSQL and Redis natively. You can spin up both inside your workflow without needing an external database, which keeps your tests isolated and fast. Medusa.js's module system makes unit testing individual modules straightforward, and you should aim to test your custom modules independently before integration tests run.
The Medusa.js community maintains active testing patterns and examples on the Medusa GitHub repository, which is worth reviewing when structuring your test suite.
Writing the Build Job
Once tests pass, your build job compiles the Medusa backend and the storefront. For a Next.js storefront consuming Medusa's APIs, the build step runs next build. For the Medusa backend itself, running medusa build generates the optimised server output.
A common mistake is caching node_modules improperly. Use GitHub's actions/cache with a key based on your package-lock.json hash. This avoids reinstalling hundreds of packages on every run while still invalidating the cache when your dependencies actually change.
Writing the Deploy Job
The deploy job runs only after the test and build jobs succeed. You can enforce this with the needs: [test, build] directive in your workflow. This sequencing ensures that a failing test blocks deployment automatically, without any manual intervention.
For deployment, your approach depends on your hosting platform.
Deploying to Railway or Render
Both Railway and Render support webhook-triggered deploys. You generate a deploy hook URL in your platform dashboard and call it from your GitHub Actions workflow using a curl command. This is the simplest approach and works well for smaller teams. The platform handles container rebuilds, migration runs, and restarts on its own after receiving the webhook.
Deploying to a VPS via SSH
For teams running their own servers, you can deploy via SSH from GitHub Actions using the appleboy/ssh-action. You store your server's SSH private key as a GitHub Secret, and your workflow connects to the server, pulls the latest code, runs npm ci and medusa migrations run, then restarts the process with PM2.
Automate your Medusa.js store today
Lets TalkRunning Database Migrations Safely
Database migrations deserve careful handling in your pipeline. Running them inside your deploy job, before the server restarts, ensures your schema is always in sync with your application code. The command medusa migrations run applies any pending migrations and is idempotent, meaning it is safe to run even when there are no pending changes.
For zero-downtime deployments, consider running migrations as a separate step before swapping traffic to the new version. This pattern works especially well when using blue-green deployment setups or platforms that support traffic routing controls.
Handling Multi-Environment Pipelines
Most production Medusa.js stores benefit from a staging environment that mirrors production. You can configure separate workflow jobs for staging and production, triggered by different branches or tags. A push to the develop branch deploys to staging, while a push to main or a tagged release deploys to production.
Keeping staging and production as close as possible reduces the chance of deployment surprises. Use the same GitHub Actions runner, the same Node.js version, and the same deploy process. Only the secrets and the deployment targets should differ.
Monitoring Your Pipeline
Once your pipeline is live, visibility into its health matters. GitHub Actions provides a built-in dashboard showing run history, step durations, and failure details. For longer pipelines, notifications via Slack or email on failure keep your team informed without requiring them to manually check every run.
Track your pipeline metrics over time. If builds consistently take longer than five minutes, look at your caching strategy first. Slow installs and uncached builds are the most common bottleneck in Medusa.js CI pipelines.
Common Mistakes to Avoid
Running migrations after the server restarts instead of before, which can cause schema mismatch errors on startup
Not pinning your Node.js version in the workflow, leading to unexpected breakage when GitHub updates runner images
Storing secrets in your workflow file directly instead of in GitHub's encrypted Secrets store
Skipping the needs: directive, which allows a broken build to proceed to deployment
Rebuilding node_modules from scratch on every run when caching would cut build times significantly
