Logo

Strapi: self-hosted headless CMS

Strapi: self-hosted headless CMS

Strapi is an open-source headless CMS, Node.js-based. REST and GraphQL APIs auto-generated. Ideal for decoupling backend (Strapi) from frontend (Next.js, Nuxt, mobile).

Introduction

Strapi lets you:

  • Define content types via web UI (Content-Type Builder)
  • Access data via auto-generated REST + GraphQL
  • Manage permissions (roles, plugins)
  • Plugin marketplace (i18n, email, S3 upload)
  • Webhooks, locales, drafts/published

Typical stack: Strapi (backend + admin) + Next.js (frontend) + PostgreSQL (db) + S3/MinIO (media).

Prerequisites

  • Linux VPS 2 vCPU, 4 GB RAM, 20 GB SSD
  • Node.js 18+
  • PostgreSQL 14+
  • Root access

Step 1: Prepare PostgreSQL

sudo apt install -y postgresql
sudo -u postgres psql

CREATE DATABASE strapi;
CREATE USER strapiuser WITH ENCRYPTED PASSWORD 'strong-pass';
GRANT ALL PRIVILEGES ON DATABASE strapi TO strapiuser;
\c strapi
GRANT ALL ON SCHEMA public TO strapiuser;
\q

Step 2: Create Strapi project

cd /var/www
npx create-strapi-app@latest strapi --quickstart --no-run

Wizard:

  • Choose between databases: postgres
  • Database name: strapi
  • Host: 127.0.0.1
  • Port: 5432
  • Username: strapiuser
  • Password: strong-pass
  • Enable SSL connection: N
cd /var/www/strapi

Step 3: First start

npm run build
npm run start

Strapi listens on :1337. Visit http://VPS_IP:1337/admin. Create first admin.

Step 4: Define a Content Type

In admin: Content-Type Builder > Create new collection type.

"Article" example:

  • title: Text
  • slug: UID (generated from title)
  • content: Rich Text (Markdown)
  • cover: Media (single)
  • author: Relation > many-to-one > User
  • publishedAt: Datetime

Save. Strapi auto-generates:

  • REST API: GET /api/articles, POST /api/articles...
  • GraphQL schema if plugin installed

Step 5: Manage permissions

Settings > Users & Permissions Plugin > Roles.

  • Public: no auth
  • Authenticated: logged in

To make articles publicly readable:

Public > Article > check find and findOne.

Step 6: Test API

curl http://VPS_IP:1337/api/articles

Filter / paginate:

curl 'http://VPS_IP:1337/api/articles?filters[title][$contains]=hello&pagination[limit]=10'

Populate relations:

curl 'http://VPS_IP:1337/api/articles?populate=cover,author'

Step 7: API token

Settings > API Tokens > Create. Type: Read-only, Full access, or custom.

curl http://VPS_IP:1337/api/articles \
    -H "Authorization: Bearer YOUR_TOKEN"

Step 8: Enable GraphQL

npm install @strapi/plugin-graphql

Restart. GraphQL playground on :1337/graphql (dev).

query {
  articles {
    data {
      attributes {
        title
        slug
        content
      }
    }
  }
}

Step 9: S3 / MinIO upload

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

config/plugins.js:

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        accessKeyId: env('AWS_ACCESS_KEY_ID'),
        secretAccessKey: env('AWS_ACCESS_SECRET'),
        region: env('AWS_REGION'),
        params: {
          Bucket: env('AWS_BUCKET'),
        },
      },
    },
  },
});

.env:

AWS_ACCESS_KEY_ID=xxx
AWS_ACCESS_SECRET=xxx
AWS_REGION=eu-west-3
AWS_BUCKET=my-bucket

Step 10: Production with PM2

sudo npm install -g pm2
cd /var/www/strapi
pm2 start npm --name strapi -- start
pm2 startup
pm2 save

Nginx reverse proxy:

server {
    listen 443 ssl http2;
    server_name strapi.your-domain.com;
    
    client_max_body_size 50M;
    
    location / {
        proxy_pass http://localhost:1337;
        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;
    }
}

Step 11: Backups

PostgreSQL:

pg_dump -U strapiuser -h 127.0.0.1 strapi | gzip > strapi-$(date +%F).sql.gz

Uploads:

tar czf strapi-uploads-$(date +%F).tar.gz /var/www/strapi/public/uploads

Strapi has export/import:

npx strapi export --no-encrypt
npx strapi import --file export.tar.gz

Step 12: Critical env vars

/var/www/strapi/.env:

HOST=0.0.0.0
PORT=1337
APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=...
ADMIN_JWT_SECRET=...
TRANSFER_TOKEN_SALT=...
JWT_SECRET=...
DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapiuser
DATABASE_PASSWORD=strong-pass

⚠️ Generate strong secrets:

openssl rand -base64 16

Troubleshooting

"ECONNREFUSED" on PostgreSQL

sudo nano /etc/postgresql/14/main/pg_hba.conf
host all all 127.0.0.1/32 md5
sudo systemctl restart postgresql

Admin panel 502

pm2 logs strapi

Run npm run build after each update.

Slow startup

Strapi 4 loads all content types, plugins, hooks at boot. ~30s is normal.

Memory issues

Increase VPS RAM, or set NODE_OPTIONS=--max-old-space-size=2048 in .env.

Useful commands

npm run develop          # dev mode (hot reload)
npm run build            # build prod
npm run start            # start prod
npm run console          # Strapi REPL

npx strapi export --no-encrypt
npx strapi import --file f.tar.gz

pm2 logs strapi
sudo tail -f /var/log/postgresql/postgresql-14-main.log

Conclusion

Strapi gives you:

  • Editable backoffice + auto API
  • Fine-grained permissions and roles
  • Multi-tenant, i18n, drafts
  • REST and GraphQL
  • Backend / frontend decoupling

Going further:

  • Pair with Next.js for frontend (ISR)
  • Enable Cloudinary for optimized media
  • For large volumes, use Strapi Cloud or K8s cluster

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members