Other CMS (Drupal, Joomla, Ghost)

Strapi Headless CMS on Domain India VPS

By Domain India Team · DomainIndia EngineeringPublished 7 min read
Knowledge base article
Contents (15 sections)

A headless CMS gives your editors a friendly admin panel and gives your developers an API, so the same content can feed a website, a mobile app and anything else. This guide takes a Strapi project from your laptop to a production server with PostgreSQL, systemd, nginx and backups.

Key takeaways

Strapi is one of the most popular open-source headless CMSs: a Node.js admin panel that generates a REST or GraphQL API from the content types you define. Develop locally, then run it on a VPS under systemd with PostgreSQL, behind nginx with SSL, with uploads in S3-compatible storage and your own off-server backups.

Headless vs traditional CMS

Traditional (WordPress)Headless (Strapi)
Single repo, HTML+CSS+JSSeparate backend (API) + frontend (any)
Template layer coupled to contentAPI-first, zero presentation
Hard to reuse across web+mobile+IoTOne API serves all clients
PHP ecosystemNode.js ecosystem
Plugins often fragileTypeScript SDK + custom controllers

Pick Strapi when:

  • You need the same content in web + mobile + email
  • You want developers to build custom frontends with React/Vue/Next/Flutter
  • Editorial team wants a friendly UI to manage content
  • You'll outgrow WordPress for content schema complexity

Requirements

  • A VPS with at least 2 GB of RAM (4 GB is more comfortable with PostgreSQL on the same server, and the admin build is memory-hungry)
  • A Node.js LTS version supported by your Strapi release (check Strapi's docs; Node.js 22 is a safe choice)
  • PostgreSQL (recommended) or MySQL/MariaDB
  • nginx for reverse proxy + SSL

Step 1 — Create Strapi project

On your laptop first (faster dev loop):

bash
npx create-strapi@latest my-cms
# Accept the default SQLite database for local development
cd my-cms && npm run develop

Visit http://localhost:1337/admin — create admin account.

Create content types (e.g. "Article" with title, body, author, thumbnail). Strapi auto-generates the API endpoints.

Test API:

bash
curl http://localhost:1337/api/articles

New content types are private: until you allow find for the Public role (Settings → Users & Permissions → Roles) or send an API token, this returns 403.

Step 2 — Prepare production config

Edit config/database.ts for production:

typescript
export default ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      host: env('DATABASE_HOST', 'localhost'),
      port: env.int('DATABASE_PORT', 5432),
      database: env('DATABASE_NAME', 'strapi'),
      user: env('DATABASE_USERNAME', 'strapi'),
      password: env('DATABASE_PASSWORD'),
      ssl: env.bool('DATABASE_SSL', false),
    },
    pool: { min: 2, max: 10 },
  },
});

config/server.ts:

typescript
export default ({ env }) => ({
  host: env('HOST', '127.0.0.1'),   // only nginx should reach Strapi
  port: env.int('PORT', 1337),
  url: env('PUBLIC_URL', 'https://cms.yourcompany.com'),
  proxy: { koa: true },   // behind nginx (Strapi 4 uses: proxy: true)
  app: {
    keys: env.array('APP_KEYS'),
  },
});

Step 3 — Setup VPS

bash
# AlmaLinux / Rocky, as root
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo dnf install -y epel-release
sudo dnf install -y nodejs postgresql-server postgresql-contrib nginx certbot python3-certbot-nginx git
# or Ubuntu / Debian:
# curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
# sudo apt install -y nodejs postgresql nginx certbot python3-certbot-nginx git

# PostgreSQL setup (AlmaLinux; Ubuntu initialises it for you)
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql
# Use a long random password, not the placeholder below
sudo -u postgres psql <<EOF
CREATE USER strapi WITH PASSWORD 'use-a-long-random-password';
CREATE DATABASE strapi OWNER strapi;
EOF

# On AlmaLinux, pg_hba.conf defaults to "ident" for 127.0.0.1; change the
# host lines to scram-sha-256 and reload PostgreSQL so password logins work

# Create app user
sudo useradd -m -s /bin/bash strapi
sudo su - strapi

Step 4 — Deploy code

bash
# As strapi user
cd ~
git clone https://github.com/yourcompany/my-cms.git
cd my-cms

npm ci

Create .env:

ini
HOST=127.0.0.1
PORT=1337
NODE_ENV=production
PUBLIC_URL=https://cms.yourcompany.com

DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=use-a-long-random-password
DATABASE_SSL=false

APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=random-long-string
ADMIN_JWT_SECRET=another-random-string
TRANSFER_TOKEN_SALT=yet-another
JWT_SECRET=one-more-random-string

Protect the file with chmod 600 .env, and generate each random value with:

bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Build the admin panel:

bash
NODE_ENV=production npm run build

Don't open the admin panel to the internet yet: whoever reaches /admin first on a new install can register the first administrator. You'll create the admin account at the end of Step 6, once nginx and SSL are in place.

Step 5 — systemd service

/etc/systemd/system/strapi.service:

ini
[Unit]
Description=Strapi CMS
After=network.target postgresql.service

[Service]
Type=simple
User=strapi
Group=strapi
WorkingDirectory=/home/strapi/my-cms
ExecStart=/usr/bin/node /home/strapi/my-cms/node_modules/.bin/strapi start
Restart=on-failure
RestartSec=5

EnvironmentFile=/home/strapi/my-cms/.env

# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/home/strapi

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Start:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now strapi
sudo journalctl -u strapi -f

Step 6 — nginx reverse proxy

/etc/nginx/conf.d/strapi.conf:

nginx
upstream strapi { server 127.0.0.1:1337; }

server {
    listen 80;
    server_name cms.yourcompany.com;

    client_max_body_size 100M;   # for uploads

    location / {
        proxy_pass http://strapi;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_read_timeout 120s;
    }
}

Test + SSL:

bash
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d cms.yourcompany.com

Now open https://cms.yourcompany.com/admin straight away and create your administrator account.

Step 7 — Frontend integration

Generate API token in Admin → Settings → API Tokens.

Next.js:

typescript
async function getArticles() {
  const res = await fetch(`${process.env.STRAPI_URL}/api/articles?populate=*`, {
    headers: { Authorization: `Bearer ${process.env.STRAPI_TOKEN}` },
    next: { revalidate: 60 },
  });
  const json = await res.json();
  return json.data;
}

Vue/Nuxt:

Anything in client-side code is public, so use a read-only API token here, or call Strapi from server routes and keep tokens on the server.

javascript
const { data } = await useFetch('/api/articles?populate=*', {
  baseURL: 'https://cms.yourcompany.com',
  headers: { Authorization: `Bearer ${config.strapiToken}` },
});

React Native:

Same pattern; use TanStack Query or SWR for caching, and a read-only token.

Step 8 — Media uploads → S3/R2

By default Strapi stores uploads on local disk, so a rebuilt or lost server loses your media. Use object storage:

bash
npm install @strapi/provider-upload-aws-s3

config/plugins.ts:

typescript
export default ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        s3Options: {
          credentials: {
            accessKeyId: env('AWS_ACCESS_KEY_ID'),
            secretAccessKey: env('AWS_SECRET_ACCESS_KEY'),
          },
          region: 'auto',
          endpoint: env('AWS_ENDPOINT'),   // e.g. Cloudflare R2 endpoint
          params: { Bucket: env('AWS_BUCKET') },
        },
      },
    },
  },
});

Also add your bucket's public domain to the img-src and media-src lists of the security middleware in config/middlewares.ts, or the admin panel won't show thumbnails. See our Cloudflare Workers and R2 article for storage without egress fees.

Step 9 — Backups

Cron entries must fit on one line, so put the steps in a script. /home/strapi/backup.sh (make it executable with chmod 700, and put the database password in /home/strapi/.pgpass with chmod 600):

bash
#!/bin/bash
set -euo pipefail
mkdir -p /home/strapi/backups
d=$(date +%Y%m%d)
pg_dump -h localhost -U strapi strapi | gzip > /home/strapi/backups/db-$d.sql.gz
tar czf /home/strapi/backups/uploads-$d.tar.gz -C /home/strapi/my-cms/public/uploads .
find /home/strapi/backups -name "*.gz" -mtime +30 -delete

Then /etc/cron.d/strapi-backup:

bash
0 3 * * * strapi /home/strapi/backup.sh

Backups on the same server don't protect you if the server is lost. Copy them off the VPS every day; see Automated backups with cron and rclone.

Plugins worth installing

  • Internationalisation (i18n) — multi-language content (built into Strapi 5)
  • Users & Permissions — JWT auth for your API's end users (built in)
  • GraphQL plugin — GraphQL alongside REST
  • SEO plugin (community) — meta fields per entry
  • Meilisearch or Algolia plugin — search

Install only what you need, and rebuild the admin panel after each plugin.

Common pitfalls

No rebuild after a plugin install
The admin panel won't change until you run NODE_ENV=production npm run build again.
SQLite in production
Locking problems under concurrent writes. Use PostgreSQL or MySQL.
Uploads on local disk
A lost or rebuilt server loses your media. Use an S3 or R2 upload provider.
Upgrading without a backup
Strapi migrates the database when a new version starts. Take a pg_dump before every upgrade.
Slow admin panel
Usually the admin was not built for production. Build it with NODE_ENV=production.
Memory creeping up
Watch memory with systemctl status strapi or your monitoring, and investigate plugins before you schedule restarts.

Running this on Domain India

  • VPS: this guide is written for a Domain India VPS, which is self-managed with full root access, from ₹553 a month excluding 18% GST. You install and secure the stack yourself, and VPS plans include no backups or snapshots, which is why Step 9 matters.
  • App Platform: Node.js apps are detected automatically and every plan includes PostgreSQL and free SSL, so Strapi is a possible fit. Test it first, and keep uploads in S3-compatible storage rather than on the app's local disk. See the App Platform guide.
  • Shared hosting: Strapi runs as its own long-lived Node.js server with a memory-hungry admin build, which shared hosting isn't designed for. Use a VPS or the App Platform.

FAQ

Strapi vs Directus vs Payload CMS?

Strapi has the largest ecosystem and plugin marketplace. Directus is database-first and works on top of an existing SQL schema. Payload is TypeScript-native and can run inside a Next.js app. All three are valid; Strapi is the most widely used.

Strapi Cloud vs self-hosted?

Strapi Cloud is Strapi's managed hosting, priced per project; check its pricing page for current plans and regions. Self-hosting on a VPS usually costs less and gives you full control, but you handle updates, security and backups yourself.

Scalability?

For read-heavy sites, put a cache or CDN in front of the API, or have your frontend build static pages, so most requests never reach Strapi. For bigger loads, run several Strapi instances behind a load balancer with shared object storage for uploads.

Can I use Strapi for e-commerce?

Possible but not ideal — no built-in cart/checkout. Pair with Medusa.js or use a dedicated e-commerce platform.

Is Strapi free?

The Community Edition is free and open source, and covers most projects. Paid plans add features such as SSO, audit logs, content history and releases.

Ready to host your CMS? Compare VPS plans, or open a support ticket with questions about a plan.

Run your own Strapi CMS

A self-managed Domain India VPS gives you full root access to run Strapi, PostgreSQL and nginx together.

See VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app