Strapi is an open-source headless CMS built on Node.js that enables teams to manage content through a REST or GraphQL API. This guide covers deploying Strapi on a VPS running Ubuntu 22.04 LTS with production-ready configuration including reverse proxy, HTTPS, and process management.
In short: Strapi is a Node.js-based headless CMS that decouples content management from frontend presentation. We'll install Node.js, scaffold a Strapi project, set up PM2 for auto-restart, configure Nginx as a reverse proxy, secure it with Let's Encrypt HTTPS, and harden the admin panel for production use.
What is Strapi and Why Headless CMS?
Traditional CMSs like WordPress tightly couple backend (content administration) with frontend (content presentation). Headless CMS decouples them:
- Backend (Headless): Admin panel for content management; serves data via REST API or GraphQL to any client
- Frontend: Independent of the CMS; can be React, Next.js, Vue, Angular, static site generators, or even mobile apps
Key benefits of Headless CMS:
- Flexibility: Use any frontend framework and deploy to multiple channels (web, mobile, IoT)
- Performance: Frontend can be static (cached at CDN); API layer remains lightweight
- Developer Experience: Content editors use Strapi's intuitive interface; developers consume clean API
- SEO: Server-side rendering remains possible while leveraging modern JavaScript frameworks
- Scalability: Decouple scaling concerns; scale frontend and backend independently
Prerequisites
- VPS or Dedicated Server with at least 1–2 GB RAM (for production with multiple concurrent users)
- Ubuntu 22.04 LTS or compatible Linux distribution
- SSH access with sudo privileges
- Basic terminal and SSH knowledge
- Domain name (if deploying with HTTPS)
- Curl installed (for downloading install scripts)
Tip: 512 MB RAM can work for development or very low-traffic staging; production with API consumers should have 1–2 GB to avoid swapping. Monitor free -h and pm2 monit to gauge actual usage.
Step 1: Install Node.js
Strapi requires Node.js 18 or later. Use nvm (Node Version Manager) to manage multiple Node versions:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm --version
Install Node.js LTS (20 or 22 recommended):
nvm install 20 nvm use 20 node --version npm --version
Set Node.js 20 as default:
nvm alias default 20
Step 2: Create a Strapi Project
Bootstrap a new Strapi project using the official starter:
npx create-strapi-app@latest my-strapi-cms --quickstart
This creates a directory structure:
my-strapi-cms/ ├── api/ # Plugins and API extensions ├── config/ # Configuration files ├── database/ # SQLite database (default) ├── public/ # Static files served to public ├── src/ # Source code ├── package.json └── .env # Environment variables
The --quickstart flag uses SQLite and starts the dev server; we'll reconfigure for production below.
Step 3: Set Up Database and Environment Variables
SQLite vs Production Database:
| Database | Use Case | Pros | Cons |
|---|---|---|---|
| SQLite | Development | No setup, file-based, lightweight | Locks on writes, poor concurrent performance |
| PostgreSQL | Production | Robust, fast, excellent for concurrent workloads | Requires separate database server |
| MySQL/MariaDB | Production | Widely available, good performance | Requires separate database server |
For this guide, we'll use PostgreSQL (recommended):
sudo apt update sudo apt install postgresql postgresql-contrib -y sudo systemctl start postgresql sudo systemctl enable postgresql
Create a database user and database:
sudo -u postgres psql CREATE USER strapi WITH PASSWORD 'your_secure_password'; CREATE DATABASE strapi OWNER strapi; \q
Warning: Use a strong password: at least 16 characters with uppercase, lowercase, numbers, and special characters. Store it securely in a password manager. Never commit passwords to version control.
Update the .env file in your Strapi project:
nano .env
Configure for production with PostgreSQL:
NODE_ENV=production DATABASE_CLIENT=postgres DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_NAME=strapi DATABASE_USERNAME=strapi DATABASE_PASSWORD=your_secure_password JWT_SECRET=your_jwt_secret_here API_TOKEN_SALT=your_api_token_salt_here ADMIN_JWT_SECRET=your_admin_jwt_secret_here
Generate secure random strings for JWT secrets:
openssl rand -base64 24
Step 4: Build Strapi for Production
Compile TypeScript, optimize assets, and bundle for production:
npm run build
This creates a dist/ directory with optimized production code. Build time is typically 2–5 minutes depending on VPS resources.
Step 5: Install and Configure PM2
PM2 is a process manager that keeps Strapi running, auto-restarts on failure, and survives server reboots:
npm install -g pm2
Create an ecosystem.config.js file to define PM2 behavior:
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: 'strapi',
script: './node_modules/.bin/strapi',
args: 'develop',
env: {
NODE_ENV: 'production',
DATABASE_CLIENT: 'postgres',
DATABASE_HOST: 'localhost',
DATABASE_PORT: 5432,
DATABASE_NAME: 'strapi',
DATABASE_USERNAME: 'strapi',
DATABASE_PASSWORD: 'your_secure_password'
},
watch: false,
max_memory_restart: '1024M',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: './logs/err.log',
out_file: './logs/out.log',
instances: 1
}]
};
EOF
Start Strapi with PM2 in production mode:
pm2 start ecosystem.config.js --env production pm2 save pm2 startup
Verify Strapi is running:
pm2 logs strapi pm2 monit
Monitor CPU and memory in real time with pm2 monit; press q to exit.
Step 6: Configure Nginx as Reverse Proxy
Nginx will proxy traffic from port 80/443 to Strapi's local port 1337, handle SSL termination, and improve performance:
sudo apt install nginx -y sudo systemctl start nginx sudo systemctl enable nginx
Create an Nginx configuration file for Strapi:
sudo nano /etc/nginx/sites-available/strapi
Add this configuration:
upstream strapi {
server 127.0.0.1:1337;
}
server {
listen 80;
server_name yourdomain.com;
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_redirect off;
proxy_buffering off;
}
}
Enable the site and test Nginx syntax:
sudo ln -s /etc/nginx/sites-available/strapi /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx
Tip: Replace yourdomain.com with your actual domain, and ensure your DNS A record points to the VPS IP address. Verify with nslookup yourdomain.com.
Step 7: Enable HTTPS with Let's Encrypt
Install Certbot and the Nginx plugin:
sudo apt install certbot python3-certbot-nginx -y
Request a free SSL certificate and auto-update Nginx:
sudo certbot --nginx -d yourdomain.com
Answer the prompts; Certbot automatically updates your Nginx config and enables HTTPS.
Enable automatic certificate renewal:
sudo systemctl start certbot.timer sudo systemctl enable certbot.timer
Verify HTTPS is working:
curl -I https://yourdomain.com
Step 8: Security Hardening and Performance Tuning
Admin Panel Security:
- Set a strong admin password (16+ characters, mixed case, numbers, symbols)
- Disable public user registration (Admin → Settings → Users & Permissions → Roles → Public)
- Use API tokens for programmatic access; granularly set permissions for each token
- Regularly audit active sessions and revoke unused tokens
Firewall Configuration (ufw):
sudo ufw enable sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS
Performance Best Practices:
- Monitor memory and CPU:
pm2 monit,htop - Cache static assets in Nginx (set Cache-Control headers)
- Use a CDN (e.g., Cloudflare) to cache and serve static assets globally
- Enable gzip compression in Nginx
- Tune database connection pooling in Strapi configuration
- Always set
NODE_ENV=productionto disable debug logs and optimize code
Warning: Never expose .env files publicly. Configure Nginx to deny access to dot files (files starting with a period) and ensure they are not in the public directory. Also keep your Git repository private and use .gitignore to exclude .env.
Content-Type Builder and Content Schema Design
Once Strapi is running, the core job of your development team is designing "Content Types" through the admin panel's Content-Type Builder — essentially designing database tables without writing raw SQL. Strapi splits Content Types into two main kinds: Collection Type for repeatable records such as blog posts, products, or customer reviews, and Single Type for one-off fixed data such as an About Us page or homepage settings.
Each Content Type is built from various field types: Text, Rich Text, Number, Boolean, Date, Media (for image/file uploads), Relation (linking to another Content Type, e.g., an article linked to one author), and Component (reusable field groups, such as SEO metadata shared across multiple pages). Getting the schema design right early reduces future migration headaches — restructuring a Content Type after substantial real data exists can corrupt existing records or require manual data migration.
Tip: Before creating Content Types on production, prototype them on a development environment first, and export the schema files (Strapi stores schemas at src/api/*/content-types/*/schema.json) to commit into Git. This lets your team review schema changes through pull requests just like regular code.
Safely Upgrading to New Strapi Versions
Strapi releases major versions periodically, and these sometimes include breaking changes affecting your API or installed plugins. Before upgrading on production, always follow this process: first, back up your entire PostgreSQL database with pg_dump and separately back up your media uploads folder. Second, always test the upgrade on a staging environment with data closely mirroring production before touching the live site. Third, read Strapi's official migration guide, since each major version typically includes specific instructions for migrating plugins or changed APIs.
When you're ready to upgrade production, enable maintenance mode or notify users in advance if your site has significant traffic. Then run npm install @strapi/strapi@latest followed by npm run build to recompile, and restart the PM2 process with pm2 restart strapi. Immediately after upgrading, check logs with pm2 logs strapi --lines 100 to spot any errors from plugins that don't yet support the new version. If you hit an unresolvable issue, you can roll back instantly to your database backup and previous codebase version — which is exactly why backing up before upgrading is a step you should never skip.
Backing Up Strapi on a Regular Schedule
Beyond backing up before version upgrades, set up a recurring automated backup schedule for production, since the content your team creates daily is valuable and hard to recover if the server fails. There are two main things to back up: the PostgreSQL database holding all your content, and the media uploads folder holding user-uploaded images and documents. Back up the database with pg_dump -U strapi strapi > backup-$(date +%Y%m%d).sql, ideally scheduled as a nightly cron job, then copy the backup file off-server — to cloud storage or a second VPS — to protect against total primary server failure.
For the media uploads folder, use rsync or tar for periodic backups, or for higher reliability, consider moving media storage from local disk to S3-compatible object storage. Strapi supports this through its upload provider plugin system, decoupling media files from any single server and reducing disk I/O load on your primary VPS.
Summary
You now have a production-ready Strapi deployment: Node.js installed via nvm (version 20 LTS), Strapi project with PostgreSQL database, PM2 managing the Strapi process with auto-restart, Nginx reverse proxy on ports 80/443, HTTPS via Let's Encrypt with auto-renewal, and firewall hardening with strong admin credentials.
Your Strapi admin panel is accessible at https://yourdomain.com/admin, and frontend applications can consume the REST/GraphQL API from any language or framework. Monitor logs with pm2 logs strapi and track resource usage with pm2 monit to ensure stable operation.
Frequently Asked Questions
Why use PM2 instead of running Strapi directly?
PM2 monitors the Strapi process and automatically restarts it if it crashes or if the VPS reboots. It also provides centralized logging and resource monitoring, reducing the need for manual intervention in production.
Can I migrate from SQLite to PostgreSQL after deploying?
Yes, but it requires exporting data from SQLite and importing into PostgreSQL. Strapi provides migration guides. It's simpler to set up the target database from the start before deploying to production.
What if my Strapi process consumes too much memory and PM2 restarts it?
Increase the max_memory_restart threshold in ecosystem.config.js, or upgrade your VPS to more RAM. Also check for memory leaks in custom plugins or API extensions and optimize database queries.
Do I need a CDN for Strapi?
For the API itself, CDN caching is limited since responses are dynamic. However, CDN is beneficial for serving static assets from your frontend. Configure your frontend to use a CDN for these resources, not the Strapi API.
Start Your AsiaGB VPS Today
AsiaGB VPS runs on SSD storage across every plan, with Thailand or Singapore datacenter options and full root access from day one — starting at 500 THB/month (Linux), backed by a Thai support team.
See VPS Plans