Technology Encyclopedia Home > How to Deploy Strapi Headless CMS on a Cloud Server — API-First Content Management

How to Deploy Strapi Headless CMS on a Cloud Server — API-First Content Management

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.


Table of Contents

  1. What Strapi Is and When to Use It
  2. Prerequisites
  3. Part 1 — Server Setup
  4. Part 2 — Install Node.js and PostgreSQL
  5. Part 3 — Create a Strapi Project
  6. Part 4 — Configure Strapi for Production
  7. Part 5 — Run with PM2
  8. Part 6 — Configure Nginx
  9. Part 7 — Enable HTTPS
  10. Part 8 — Build the Admin UI for Production
  11. The Gotcha: Admin Panel Build Required for Production
  12. API Usage Examples

  • Key Takeaways
  • Use the appropriate Lighthouse application image to skip manual installation steps where available
  • Lighthouse snapshots provide one-click full-server backup before major changes
  • OrcaTerm browser terminal lets you manage the server from any device
  • CBS cloud disk expansion handles growing storage needs without server migration
  • Console-level firewall + UFW = two independent protection layers

What Strapi Is and When to Use It {#what}

Strapi is the right choice when:

  • You need content delivered via API to multiple frontends (web, mobile, smart TV)
  • You want non-technical editors to manage content without coding
  • You need custom content types (e.g., Blog Post, Product, Author, Category)
  • You're building a JAMstack or decoupled architecture
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

Prerequisites {#prerequisites}

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

Part 1 — Server Setup {#part-1}

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

Part 2 — Install Node.js and PostgreSQL {#part-2}

# 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

Part 3 — Create a Strapi Project {#part-3}

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

Part 4 — Configure Strapi for Production {#part-4}

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

Part 5 — Run with PM2 {#part-5}

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

Part 6 — Configure Nginx {#part-6}

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

Part 7 — Enable HTTPS {#part-7}

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


Part 8 — Build the Admin UI for Production {#part-8}

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 Gotcha: Admin Panel Build Required for Production {#gotcha}

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.


API Usage Examples {#api-examples}

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.

Troubleshooting {#troubleshooting}

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

Frequently Asked Questions {#faq}

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.

How do I secure the admin panel?
Change the default admin URL if possible, use a strong unique password, enable two-factor authentication, and consider restricting admin access by IP in your Nginx configuration.

Deploy your headless CMS today:
👉 Tencent Cloud Lighthouse — Node.js-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers