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/findOneon content meant to be public. - Use API tokens with minimal scopes for your front ends.
- Consider limiting
/adminby IP or VPN. - Keep Strapi updated (
npx @strapi/upgradefor 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.