Why Self-Host Strapi Headless CMS on Your Own Infrastructure?
Headless CMS architectures have become the gold standard for decoupling content management from modern frontend frameworks like Next.js, Nuxt, Astro, and mobile Flutter/React Native apps. Strapi is the leading open-source Node.js headless CMS, offering an intuitive administrative UI, dynamic content-type builders, automated GraphQL/REST API generation, and granular role-based permissions. While managed SaaS platforms charge hundreds of dollars per month with strict record limits, self-hosting Strapi on a cloud Linux VPS gives you unlimited content types, full database ownership, custom plugins, and complete privacy at zero recurring software costs.
In this end-to-end production deployment guide, we will configure Strapi on Ubuntu 24.04/22.04 LTS backed by enterprise PostgreSQL, managed by PM2 process manager for continuous background execution, secured behind an Nginx reverse proxy, and encrypted with Let’s Encrypt SSL.
Step 1: Installing Node.js LTS Runtimes and Build Tooling
Strapi requires active Node.js LTS versions (Node.js 18 or 20) along with essential native C++ build tools like gcc, g++, and make to compile database bindings:
# Install Node.js 20 LTS via official NodeSource repository
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt update && sudo apt install -y nodejs build-essential git
# Verify Node and NPM runtime versions
node -v
npm -v
# Install PM2 process manager globally
sudo npm install -g pm2
Step 2: Installing and Hardening PostgreSQL Database
Production Strapi deployments should always utilize PostgreSQL instead of lightweight SQLite for superior concurrency, ACID transaction guarantees, and reliable automated backups:
# Install PostgreSQL server and client libraries
sudo apt install -y postgresql postgresql-contrib
# Switch to postgres user and create dedicated database & user
sudo -u postgres psql << 'EOF'
CREATE DATABASE strapi_production;
CREATE USER strapi_admin WITH ENCRYPTED PASSWORD 'UltraSecureDbPass2026!';
GRANT ALL PRIVILEGES ON DATABASE strapi_production TO strapi_admin;
ALTER DATABASE strapi_production OWNER TO strapi_admin;
\q
EOF
Step 3: Initializing and Building the Strapi Project
Create your Strapi application in a standardized production directory like /var/www/strapi-app:
cd /var/www
# Initialize Strapi with no quickstart template to connect PostgreSQL
npx create-strapi-app@latest strapi-app --no-run
# Navigate into project directory
cd /var/www/strapi-app
Create a production environment file at /var/www/strapi-app/.env with cryptographically secure random keys:
HOST=127.0.0.1
PORT=1337
# App security keys
APP_KEYS=generate_random_key_1,generate_random_key_2
API_TOKEN_SALT=generate_random_salt_1
ADMIN_JWT_SECRET=generate_random_admin_secret
TRANSFER_TOKEN_SALT=generate_random_transfer_salt
JWT_SECRET=generate_random_jwt_secret
# Database Configuration
DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi_production
DATABASE_USERNAME=strapi_admin
DATABASE_PASSWORD=UltraSecureDbPass2026!
DATABASE_SSL=false
Build the production admin panel bundle:
NODE_ENV=production npm run build
Step 4: Configuring PM2 Cluster Daemon and Ecosystem
To ensure Strapi starts automatically upon server reboot and restarts automatically if memory thresholds are exceeded, create a PM2 ecosystem file /var/www/strapi-app/ecosystem.config.js:
module.exports = {
apps: [
{
name: 'strapi-cms',
cwd: '/var/www/strapi-app',
script: 'npm',
args: 'run start',
env: {
NODE_ENV: 'production',
},
instances: 1,
autorestart: true,
max_memory_restart: '800M',
error_file: '/var/log/strapi-error.log',
out_file: '/var/log/strapi-out.log',
},
],
};
Start the daemon and generate the systemd startup hook:
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd
Step 5: Nginx Reverse Proxy with WebSocket & File Upload Buffers
Strapi requires handling large media library uploads and real-time dashboard updates. Configure /etc/nginx/sites-available/cms.example.com:
server {
listen 80;
server_name cms.example.com;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:1337;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $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 Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
Enable the virtual host and acquire a free Let’s Encrypt SSL certificate:
sudo ln -s /etc/nginx/sites-available/cms.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d cms.example.com
Production Hardening & Performance Checklist
- Offload Media Uploads to S3/R2: Install
@strapi/provider-upload-aws-s3to store media library images on cloud object storage rather than filling local VPS storage. - Enable Response Caching: Use Strapi REST cache plugins or Cloudflare Edge caching to serve static API responses in sub-20ms.
- Setup Automated PostgreSQL Backups: Schedule daily
pg_dumpcron jobs encrypted and synchronized to offsite storage.
Automating Production Media Backups with S3-Compatible Object Storage
When running a headless CMS in production, storing user-uploaded media files (high-resolution images, PDF documents, video clips) directly on the local VPS SSD can quickly exhaust local disk capacity and complicate disaster recovery. The recommended industry practice is to offload media storage to S3-compatible cloud object storage (such as Cloudflare R2, AWS S3, or Backblaze B2).
Install the official S3 upload provider plugin inside your Strapi application directory:
# Install Strapi S3 provider
npm install @strapi/provider-upload-aws-s3 --save
Configure the provider in /var/www/strapi-app/config/plugins.js:
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
s3Options: {
accessKeyId: env('AWS_ACCESS_KEY_ID'),
secretAccessKey: env('AWS_ACCESS_SECRET'),
region: env('AWS_REGION'),
endpoint: env('AWS_ENDPOINT'),
params: {
Bucket: env('AWS_BUCKET'),
},
},
},
actionOptions: {
upload: {},
uploadStream: {},
delete: {},
},
},
},
});
Securing Strapi Headless CMS with Web Application Firewalls
Exposing an administrative CMS to the public internet requires robust defenses against credential stuffing and brute-force attacks. Implement rate limiting and security headers in your Nginx reverse proxy configuration:
# Rate limit zone: 10 requests per second per IP
limit_req_zone $binary_remote_addr zone=strapi_limit:10m rate=10r/s;
server {
server_name cms.example.com;
# Apply rate limiting to admin login endpoints
location /admin {
limit_req zone=strapi_limit burst=15 nodelay;
proxy_pass http://127.0.0.1:1337;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Recommended Related Technical Guides
Host Strapi & Node.js on Fast CpanelFree Cloud Servers
Get dedicated RAM, lightning-fast NVMe storage, and 99.9% uptime for your headless CMS and API backends with 100% free hosting and VPS options.

