I kept running into the same problem building web apps: content needed to be editable by non-developers, but the app was a React or Next.js frontend that didn't have a CMS. Building a custom admin panel felt wasteful. Integrating a traditional CMS felt like overkill.
Strapi was the answer. You define your content types through a visual admin UI, and Strapi automatically generates the REST and GraphQL APIs your frontend consumes. Non-technical content editors get a clean admin interface, developers get a structured API, and there's no custom backend code to write.
Deploying it on your own server means no content API rate limits, no vendor lock-in, and the ability to customize it in ways the cloud version doesn't allow.
This guide deploys Strapi on Ubuntu 22.04 with PostgreSQL, PM2 for process management, Nginx as the reverse proxy, and HTTPS.
I run Strapi on Tencent Cloud Lighthouse. The 2 vCPU / 4 GB RAM plan handles a production Strapi instance comfortably. Strapi's managed cloud starts at $29/month — self-hosting on Lighthouse at a lower cost removes that ceiling and gives you full control over plugins, custom code, and the admin panel configuration. The snapshot feature is particularly useful before Strapi version upgrades, which can have database migration steps, and OrcaTerm lets you check Strapi's startup logs or run CLI commands from any browser.
- Key Takeaways
Strapi is the right choice when:
| Strapi | Traditional CMS (WordPress) |
|---|---|
| API-first | Page-rendering focused |
| Any frontend | PHP templates |
| GraphQL + REST | REST only |
| Content types via UI | Plugin-based |
| No built-in frontend | Built-in frontend |
| Requirement | Notes |
|---|---|
| Cloud server | Tencent Cloud Lighthouse Ubuntu 22.04 |
| Node.js 18+ | Strapi requires Node.js 18 or 20 LTS |
| PostgreSQL | Recommended for production |
| 2 GB+ RAM | Strapi admin panel build needs memory |
ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y git nginx
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable
# Node.js 20 LTS via NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh"
nvm install 20 && nvm use 20 && nvm alias default 20
npm install -g pm2
# PostgreSQL
sudo apt install -y postgresql postgresql-contrib
sudo systemctl start postgresql && sudo systemctl enable postgresql
# Create database and user
sudo -u postgres psql << 'EOF'
CREATE DATABASE strapi_db;
CREATE USER strapi_user WITH PASSWORD 'choose_strong_password';
GRANT ALL PRIVILEGES ON DATABASE strapi_db TO strapi_user;
\c strapi_db
GRANT ALL ON SCHEMA public TO strapi_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO strapi_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO strapi_user;
EOF
mkdir -p ~/apps && cd ~/apps
# Create new Strapi project
npx create-strapi-app@latest mystrapi --quickstart --no-run
# Or without the interactive wizard:
npx create-strapi-app@latest mystrapi \
--dbclient=postgres \
--dbhost=127.0.0.1 \
--dbport=5432 \
--dbname=strapi_db \
--dbusername=strapi_user \
--dbpassword=choose_strong_password \
--no-run
If you have an existing Strapi project, clone it:
git clone https://github.com/your-username/your-strapi-project.git mystrapi
cd mystrapi
npm install
Create ~/apps/mystrapi/.env:
nano ~/apps/mystrapi/.env
HOST=0.0.0.0
PORT=1337
APP_KEYS=generate_four_random_keys_separated_by_commas
API_TOKEN_SALT=generate_random_string
ADMIN_JWT_SECRET=generate_random_string
TRANSFER_TOKEN_SALT=generate_random_string
JWT_SECRET=generate_random_string
DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi_db
DATABASE_USERNAME=strapi_user
DATABASE_PASSWORD=your_database_password
DATABASE_SSL=false
NODE_ENV=production
Generate random strings for secrets:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Run multiple times, use different values for each secret
Configure the database in config/database.js:
module.exports = ({ env }) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST', '127.0.0.1'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi_db'),
user: env('DATABASE_USERNAME', 'strapi_user'),
password: env('DATABASE_PASSWORD'),
ssl: env.bool('DATABASE_SSL', false),
},
},
});
Install the PostgreSQL client:
cd ~/apps/mystrapi
npm install pg
Build the admin panel first (see also Part 8):
cd ~/apps/mystrapi
NODE_ENV=production npm run build
Start with PM2:
pm2 start npm --name "strapi" -- start
pm2 startup && pm2 save
Verify it's running:
pm2 status
pm2 logs strapi
# Test locally
curl http://localhost:1337/api
sudo nano /etc/nginx/sites-available/strapi
server {
listen 80;
server_name cms.yourdomain.com;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:1337;
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_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
}
}
sudo ln -s /etc/nginx/sites-available/strapi /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d cms.yourdomain.com
Update the Strapi .env to reflect the HTTPS URL:
PUBLIC_URL=https://cms.yourdomain.com
Restart Strapi: pm2 restart strapi
The Strapi admin panel must be built before production use. The development mode auto-builds, but in production with NODE_ENV=production you must build explicitly:
cd ~/apps/mystrapi
NODE_ENV=production npm run build
pm2 restart strapi
The build compiles the React-based admin UI into static assets. This takes 2-5 minutes depending on server speed. After building, the admin panel is served at https://cms.yourdomain.com/admin.
Set up auto-rebuild for deployments:
nano ~/deploy-strapi.sh
#!/bin/bash
set -e
cd ~/apps/mystrapi
git pull origin main
npm install
NODE_ENV=production npm run build
pm2 restart strapi
echo "Strapi deployed: $(date)"
chmod +x ~/deploy-strapi.sh
The most common confusion with Strapi: you start it in production mode and get a blank white page at /admin, or the admin styles look broken.
The cause: in production, Strapi expects pre-built static assets from npm run build. The development auto-build doesn't run when NODE_ENV=production.
Debug:
# Check if admin assets exist
ls ~/apps/mystrapi/build/
# If empty or missing, run the build
cd ~/apps/mystrapi && NODE_ENV=production npm run build
pm2 restart strapi
Also ensure NODE_ENV=production is set in .env and in the PM2 start command, not just at the shell level.
After creating content types in the admin UI (https://cms.yourdomain.com/admin), the API is available automatically:
# Get all articles (public endpoint)
curl https://cms.yourdomain.com/api/articles
# Get a single article by ID
curl https://cms.yourdomain.com/api/articles/1
# Authenticated request (with API token)
curl https://cms.yourdomain.com/api/articles \
-H "Authorization: Bearer YOUR_API_TOKEN"
# Create an article
curl -X POST https://cms.yourdomain.com/api/articles \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "My Article", "content": "Article body here"}}'
# GraphQL (if enabled)
curl https://cms.yourdomain.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ articles { data { id attributes { title } } } }"}'
Generate API tokens at: Admin → Settings → API Tokens → Create new API Token.
| Issue | Likely Cause | Fix |
|---|---|---|
| Connection refused | Service not running or wrong port | Check systemctl status SERVICE and verify firewall rules |
| Permission denied | Wrong file ownership or permissions | Check file ownership with ls -la and use chown/chmod to fix |
| 502 Bad Gateway | Backend service not running | Restart the backend service; check logs with journalctl -u SERVICE |
| SSL certificate error | Certificate expired or domain mismatch | Run sudo certbot renew and verify domain DNS points to server IP |
| Service not starting | Config error or missing dependency | Check logs with journalctl -u SERVICE -n 50 for specific error |
| Out of disk space | Logs or data accumulation | Run df -h to identify usage; clean logs or attach CBS storage |
| High memory usage | Too many processes or memory leak | Check with htop; consider upgrading instance plan if consistently high |
| Firewall blocking traffic | Port not open in UFW or Lighthouse console | Open port in Lighthouse console firewall AND sudo ufw allow PORT |
Can I migrate an existing Strapi site to a cloud server?
Yes. Export your content and database from the current host, import them on the new server, then update your DNS to point to the new IP. Update any hardcoded URLs in the database or configuration files.
How do I keep Strapi updated securely?
Enable automatic minor updates where available, and manually update major versions after testing on a staging environment. Take a Lighthouse snapshot before any significant update.
Do I need a CDN for my Strapi site?
For international audiences, yes. A CDN like EdgeOne caches static assets at edge nodes worldwide, improving load times for visitors far from your server region.
What causes Strapi to be slow on a VPS?
Usually: missing caching plugin/configuration, unoptimized images, too many plugins, or insufficient server RAM. Start with a caching layer (Redis or built-in cache) and image optimization.
Deploy your headless CMS today:
👉 Tencent Cloud Lighthouse — Node.js-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers