← Back to blog

Deploying a Strapi CMS on Railway and a Next.js Frontend on Vercel

6 min readtutorial
deployNextJs

Setting up a headless CMS stack with a free tier is surprisingly doable in 2026. This post walks through deploying Strapi v5 on Railway (with a managed PostgreSQL database) and connecting it to a Next.js frontend on Vercel — the exact setup powering this portfolio.

What we're building

  • Strapi v5 — headless CMS running on Railway, backed by Railway-managed PostgreSQL
  • Next.js — frontend on Vercel, fetching content from Strapi via the REST API
  • pnpm monorepo — both apps live in the same repo under apps/cms/ and apps/web/

Prerequisites

  • A Railway account
  • A Vercel account
  • Node.js 20+ and pnpm installed locally
  • A GitHub repo with your Strapi + Next.js monorepo

Part 1: Deploying Strapi on Railway

1. Create a new Railway project

Go to railway.app, create a new project, and add two services:

  1. PostgreSQL — click "Add a service" → "Database" → "PostgreSQL"
  2. Your Strapi app — click "Add a service" → "GitHub Repo" → select your repo

For the Strapi service, set the Root Directory to apps/cms/ (or wherever your Strapi app lives in the monorepo).

2. Fix pnpm detection in a monorepo

This is the main gotcha. Railway uses Railpack as its build system, which detects the package manager by looking for a lockfile in the service root directory. If your pnpm-lock.yaml only exists at the monorepo root but your Railway root is set to apps/cms/, Railpack won't find it and will fall back to npm — causing the build to fail with pnpm: not found.

The fix: generate a standalone pnpm-lock.yaml inside apps/cms/ and add a packageManager field to its package.json.

# In apps/cms/
pnpm install --ignore-workspace

Then add to apps/cms/package.json:

{
  "packageManager": "pnpm@10.24.0"
}

Commit both files. Railpack will now detect pnpm correctly.

3. Configure environment variables

In the Railway dashboard, set the following environment variables on your Strapi service:

HOST=0.0.0.0
NODE_ENV=production
URL=https://your-strapi-domain.com

DATABASE_CLIENT=postgres
DATABASE_URL=${{Postgres.DATABASE_URL}}
DATABASE_SSL=false
DATABASE_SCHEMA=public

APP_KEYS=<generate with: openssl rand -base64 32,openssl rand -base64 32>
API_TOKEN_SALT=<openssl rand -base64 32>
ADMIN_JWT_SECRET=<openssl rand -base64 32>
TRANSFER_TOKEN_SALT=<openssl rand -base64 32>
ENCRYPTION_KEY=<openssl rand -base64 32>
JWT_SECRET=<openssl rand -base64 32>

${{Postgres.DATABASE_URL}} is Railway's variable reference syntax — it automatically injects the PostgreSQL connection string from your database service.

4. Fix the admin panel URL

Without the URL config, Strapi builds its admin panel links using the raw host header, which can result in broken redirects (e.g. the admin panel pointing to http://0.0.0.0:PORT/your-domain/admin).

In config/server.ts, make sure url is set:

export default ({ env }) => ({
  host: env("HOST", "0.0.0.0"),
  port: env.int("PORT", 1337),
  url: env("URL", ""),
  app: {
    keys: env.array("APP_KEYS"),
  },
});

5. Set build and start commands

In Railway service settings:

  • Build Command: pnpm run build
  • Start Command: pnpm run start

6. Add a custom domain (optional)

In Railway → your service → Settings → Domains, add your custom domain. Set up the DNS CNAME record as shown. Railway provisions a TLS certificate automatically.

7. Verify the deployment

Once deployed, visit https://your-strapi-domain.com/admin. You should see the Strapi admin login screen. Create your admin account and you're in.


Part 2: Deploying Next.js on Vercel

1. Create a Strapi API token

Before deploying the frontend, create a read-only API token in Strapi so your Next.js app can fetch content.

In the Strapi admin panel: Settings → API Tokens → Create new API Token

  • Name: Vercel Web (or anything descriptive)
  • Token type: Read-only
  • Token duration: Unlimited

Copy the token — you won't see it again.

2. Make API permissions public (optional)

If you'd rather not use a token for public content, go to Settings → Users & Permissions → Roles → Public and enable find and findOne for each content type you want to expose. This is simpler but less secure.

3. Configure next.config.ts for image hosting

Strapi serves uploaded images from its own domain. Tell Next.js to allow images from your Strapi host:

// next.config.ts
const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "your-strapi-domain.com",
        pathname: "/uploads/**",
      },
    ],
  },
};

4. Add vercel.json for monorepo support

If your Next.js app is inside a monorepo (e.g. apps/web/), create a vercel.json at the repo root:

{
  "framework": "nextjs",
  "buildCommand": "pnpm --filter=web build",
  "installCommand": "pnpm install",
  "outputDirectory": "apps/web/.next"
}

This tells Vercel to:

  • Run pnpm install at the repo root (which resolves all workspace packages)
  • Run the build script only for the web workspace
  • Pick up the output from apps/web/.next

5. Import the project on Vercel

  1. Go to vercel.com/new
  2. Import your GitHub repo
  3. Vercel will auto-detect the vercel.json and configure the build settings
  4. Add the following environment variables before the first deploy:
VariableValue
NEXT_PUBLIC_STRAPI_URLhttps://your-strapi-domain.com
STRAPI_API_TOKENThe token you created in step 1
NEXT_PUBLIC_SITE_URLhttps://your-vercel-domain.com
  1. Click Deploy

6. Verify the deployment

Once deployed, your Next.js app should be live and fetching content from Strapi. Any content you publish in the Strapi admin will appear on the site within 60 seconds (the default ISR revalidation interval).


Connecting the two: CORS

If you're making client-side requests from your Next.js app to Strapi (e.g. for a comment form), you'll need to allow your Vercel domain in Strapi's CORS config.

In config/middlewares.ts:

export default [
  "strapi::errors",
  {
    name: "strapi::security",
    config: {
      contentSecurityPolicy: {
        useDefaults: true,
        directives: {
          "connect-src": ["'self'", "https:"],
        },
      },
    },
  },
  {
    name: "strapi::cors",
    config: {
      origin: ["https://your-vercel-domain.com"],
    },
  },
  // ... rest of defaults
];

Summary

Strapi CMSNext.js Frontend
HostRailwayVercel
DatabaseRailway PostgreSQL
Build detectionRailpack (needs lockfile in service root)Vercel (via vercel.json)
Key env varsDATABASE_URL, URL, APP_KEYS, secretsNEXT_PUBLIC_STRAPI_URL, STRAPI_API_TOKEN
Custom domainRailway → Settings → DomainsVercel → Settings → Domains

The biggest non-obvious issue is the Railpack lockfile detection in a monorepo — once you know about it, the fix is straightforward. Everything else is standard configuration.

Leave a Comment

Not displayed publicly.