Strapi Headless CMS on DomainIndia VPS
Headless vs traditional CMS
| Traditional (WordPress) | Headless (Strapi) |
|---|---|
| Single repo, HTML+CSS+JS | Separate backend (API) + frontend (any) |
| Template layer coupled to content | API-first, zero presentation |
| Hard to reuse across web+mobile+IoT | One API serves all clients |
| PHP ecosystem | Node.js ecosystem |
| Plugins often fragile | TypeScript 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
- DomainIndia VPS (2+ GB RAM; 4 GB for production with Postgres)
- Node.js 20+
- PostgreSQL (recommended) or MySQL/MariaDB
- nginx for reverse proxy + SSL
Step 1 — Create Strapi project
On your laptop first (faster dev loop):
npx create-strapi-app@latest my-cms --quickstart
# Quickstart uses SQLite (fine for dev)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:
curl http://localhost:1337/api/articlesStep 2 — Prepare production config
Edit config/database.ts for production:
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:
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'https://cms.yourcompany.com'),
proxy: true, // behind nginx
app: {
keys: env.array('APP_KEYS'),
},
});Step 3 — Setup VPS
# On VPS as root
sudo dnf install -y nodejs postgresql-server postgresql-contrib nginx certbot python3-certbot-nginx git
# or Ubuntu:
# curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
# sudo apt install -y nodejs postgresql nginx certbot python3-certbot-nginx git
# PostgreSQL setup
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql
sudo -u postgres psql <<EOF
CREATE DATABASE strapi;
CREATE USER strapi WITH ENCRYPTED PASSWORD 'changeme';
GRANT ALL PRIVILEGES ON DATABASE strapi TO strapi;
EOF
# Create app user
sudo useradd -m -s /bin/bash strapi
sudo su - strapiStep 4 — Deploy code
# As strapi user
cd ~
git clone https://github.com/yourcompany/my-cms.git
cd my-cms
npm ciCreate .env:
HOST=0.0.0.0
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=changeme
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-stringGenerate random values:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Build the admin panel:
NODE_ENV=production npm run buildFirst run to create admin:
npm start
# Visit https://cms.yourcompany.com/admin — create admin account
# Ctrl+C when doneStep 5 — systemd service
/etc/systemd/system/strapi.service:
[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.targetStart:
sudo systemctl daemon-reload
sudo systemctl enable --now strapi
sudo journalctl -u strapi -fStep 6 — nginx reverse proxy
/etc/nginx/conf.d/strapi.conf:
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:
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d cms.yourcompany.comStep 7 — Frontend integration
Generate API token in Admin → Settings → API Tokens.
Next.js:
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:
const { data } = await useFetch('/api/articles?populate=*', {
baseURL: 'https://cms.yourcompany.com',
headers: { Authorization: `Bearer ${config.strapiToken}` },
});React Native:
Same pattern; use react-query or swr for caching.
Step 8 — Media uploads → S3/R2
By default Strapi stores uploads on local disk — doesn't survive VPS rebuild. Use object storage:
npm install @strapi/provider-upload-aws-s3config/plugins.ts:
export default ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
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') },
},
},
},
});See our Cloudflare R2 article for zero-egress storage setup.
Step 9 — Backups
Daily Postgres + uploads backup:
# /etc/cron.d/strapi-backup
0 3 * * * strapi /bin/bash -c '
pg_dump -h localhost -U strapi strapi | gzip > /home/strapi/backups/db-$(date +%Y%m%d).sql.gz &&
tar czf /home/strapi/backups/uploads-$(date +%Y%m%d).tar.gz -C /home/strapi/my-cms/public/uploads . &&
find /home/strapi/backups -name "*.gz" -mtime +30 -delete
'See Automated Backups for off-site copies.
Plugins worth installing
- Internationalization (i18n) — multi-language content
- Users & Permissions — JWT auth for API
- Content Versioning — track edits
- SEO plugin — meta tags per entry
- GraphQL plugin — GraphQL alongside REST
- Meilisearch / Algolia plugin — search
Common pitfalls
FAQ
Strapi — largest ecosystem, most plugins. Directus — SQL-first, excellent DB integration. Payload — TypeScript-native, newer but very clean. All valid; Strapi is the safest pick in 2026.
Strapi Cloud ($15-$300+/mo) — zero ops but US-based. Self-host on DomainIndia VPS — lower cost, data sovereignty in India, more setup work.
Strapi handles 1000+ req/sec on a 4 GB VPS with caching. For bigger loads, horizontal scale with load balancer + Redis session/cache.
Possible but not ideal — no built-in cart/checkout. Pair with Medusa.js or use a dedicated e-commerce platform.
Community Edition (free, self-host) is feature-complete. Enterprise Edition adds SSO, audit logs, premium support.
Run your own Strapi CMS on a DomainIndia VPS — full control, data in India. Get started