Strapi is an open-source headless CMS built on Node.js: editors manage content in an admin panel, and your websites and apps fetch it via REST or GraphQL APIs. Self-hosting it on a VPS gives you full control of data and no per-seat pricing.

Requirements

  • Node.js LTS supported by your Strapi version (check the Strapi docs) — see installing Node.js.
  • PostgreSQL (recommended for production) or MySQL/MariaDB. SQLite is for development.
  • 2 GB RAM or more — building the admin panel is memory-hungry; add swap on small servers or build elsewhere.

1. Database

sudo apt install -y postgresql
sudo -u postgres psql -c "CREATE ROLE strapi WITH LOGIN PASSWORD 'a-long-random-password';"
sudo -u postgres psql -c "CREATE DATABASE strapi OWNER strapi;"

See PostgreSQL users and databases.

2. Get the project onto the server

Usually you develop locally (npx create-strapi@latest), commit to Git, and clone on the server:

sudo adduser --disabled-password --gecos "" strapi
sudo -u strapi -i
git clone [email protected]:you/cms.git ~/cms && cd ~/cms
npm ci

Content types are defined in code (src/api/**/content-types) — in production you change them by deploying code, not in the admin’s Content-Type Builder (which is disabled in production mode).

3. Environment variables

~/cms/.env (mode 600):

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

APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=random1
ADMIN_JWT_SECRET=random2
TRANSFER_TOKEN_SALT=random3
JWT_SECRET=random4
ENCRYPTION_KEY=random5

DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=a-long-random-password

Generate each secret with openssl rand -base64 32. Don’t reuse development secrets, and don’t commit .env. Variable names differ slightly between Strapi versions — check the project’s config/*.ts files for what’s read. Make sure config/server uses url: env('PUBLIC_URL') and enables proxy: true so Strapi trusts Nginx’s forwarded headers.

4. Build and run

NODE_ENV=production npm run build
NODE_ENV=production npm run start

build compiles the admin panel. Run it with PM2 for restarts and boot persistence:

npm install -g pm2
pm2 start npm --name strapi -- run start
pm2 save && pm2 startup

See the PM2 guide.

5. Nginx and HTTPS

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

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:1337;
        proxy_http_version 1.1;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d cms.example.com

client_max_body_size must allow your media uploads. Open https://cms.example.com/admin and create the first administrator immediately.

6. Media uploads

By default, uploads are stored in public/uploads on the server — back up that directory, and make sure it survives deployments (don’t wipe it with rsync --delete). For larger sites, use an upload provider plugin for S3-compatible storage (AWS S3, or self-hosted MinIO) so media lives outside the app server.

7. Security

  • Restrict who can register; review Users & Permissions roles — the Public role should only have find/findOne on content meant to be public.
  • Use API tokens with minimal scopes for your front ends.
  • Consider limiting /admin by IP or VPN.
  • Keep Strapi updated (npx @strapi/upgrade for major versions).

8. Backups

pg_dump -U strapi -h 127.0.0.1 -Fc strapi > strapi-$(date +%F).dump
tar czf uploads-$(date +%F).tar.gz -C ~/cms/public uploads

Strapi’s strapi export command can also export content and media into an encrypted archive. See pg_dump.

Deploying updates

git pull --ff-only && npm ci && NODE_ENV=production npm run build && pm2 reload strapi

Database migrations for content-type changes run automatically on start. Test upgrades on a staging copy first.

Related: Directus, headless CMS options and WordPress as a headless CMS.

  • Strapi
  • Headless CMS
  • Node.js