Deploying Strapi Headless CMS on Your Own Server: Complete Self-Hosted Guide
Step-by-step guide to self-hosting Strapi v5 headless CMS with PostgreSQL, Nginx reverse proxy, and automated backups. Save thousands compared to Strapi Cloud.
Why Self-Host Strapi?
Strapi is the leading open-source headless CMS with over 60k GitHub stars. While Strapi Cloud offers a managed experience, self-hosting gives you full control over your data, unlimited API calls, and zero per-seat pricing.
Server infrastructure: production and staging environments connected via VLAN with offsite backups.
At TechSaaS, we deploy Strapi for clients who need a flexible content API without the recurring SaaS fees. A typical Strapi Cloud plan costs $99-499/month. Self-hosted? About $5/month in resources on existing infrastructure.
Prerequisites
- A Linux server (Ubuntu 22.04+ or Debian 12+)
- Docker and Docker Compose installed
- PostgreSQL 14+ (or use the Docker setup below)
- Node.js 20 LTS (for local development)
- A domain name with DNS configured
Docker Compose Setup
Create a docker-compose.yml for your Strapi deployment:
version: "3.8"
services:
strapi:
image: node:20-alpine
working_dir: /app
command: sh -c "npm install && npm run build && npm run start"
volumes:
- ./strapi-app:/app
- strapi_node_modules:/app/node_modules
environment:
DATABASE_CLIENT: postgres
DATABASE_HOST: strapi-db
DATABASE_PORT: 5432
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: your-secure-password-here
DATABASE_SSL: "false"
APP_KEYS: key1,key2,key3,key4
API_TOKEN_SALT: your-api-token-salt
ADMIN_JWT_SECRET: your-admin-jwt-secret
TRANSFER_TOKEN_SALT: your-transfer-token-salt
JWT_SECRET: your-jwt-secret
NODE_ENV: production
ports:
- "1337:1337"
depends_on:
strapi-db:
condition: service_healthy
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.strapi.rule=Host(`cms.example.com`)"
- "traefik.http.services.strapi.loadbalancer.server.port=1337"
strapi-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: strapi
POSTGRES_USER: strapi
POSTGRES_PASSWORD: your-secure-password-here
volumes:
- strapi_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U strapi"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
strapi_data:
strapi_node_modules:
Creating a New Strapi Project
If starting from scratch, create the Strapi app locally first:
npx create-strapi@latest strapi-app \
--dbclient postgres \
--dbhost localhost \
--dbport 5432 \
--dbname strapi \
--dbusername strapi \
--dbpassword your-secure-password-here \
--no-run
Get more insights on Self-Hosted
Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.
Then copy the project directory to your server.
Production Configuration
Database Configuration
Edit config/database.ts for production-ready settings:
export default ({ env }) => ({
connection: {
client: "postgres",
connection: {
host: env("DATABASE_HOST", "127.0.0.1"),
port: env.int("DATABASE_PORT", 5432),
database: env("DATABASE_NAME", "strapi"),
user: env("DATABASE_USERNAME", "strapi"),
password: env("DATABASE_PASSWORD", ""),
ssl: env.bool("DATABASE_SSL", false),
},
pool: {
min: 2,
max: 10,
acquireTimeoutMillis: 300000,
createTimeoutMillis: 300000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
},
},
});
Server Configuration
Configure the server for production in config/server.ts:
export default ({ env }) => ({
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 1337),
url: env("PUBLIC_URL", "https://cms.example.com"),
app: {
keys: env.array("APP_KEYS"),
},
webhooks: {
populateRelations: false,
},
});
Cloud to self-hosted migration can dramatically reduce infrastructure costs while maintaining full control.
Upload Provider — Local with Backup
For self-hosted file uploads, configure the local provider with a persistent volume:
// config/plugins.ts
export default ({ env }) => ({
upload: {
config: {
providerOptions: {
localServer: {
maxage: 300000,
},
},
sizeLimit: 50 * 1024 * 1024, // 50MB
breakpoints: {
xlarge: 1920,
large: 1000,
medium: 750,
small: 500,
xsmall: 64,
},
},
},
});
Automated Backups
Create a backup script that dumps both the database and uploaded files:
#!/bin/bash
# backup-strapi.sh
BACKUP_DIR="/backups/strapi"
DATE="\$(date +%Y%m%d_%H%M%S)"
mkdir -p "\$BACKUP_DIR"
# Database dump
docker exec strapi-db pg_dump -U strapi strapi | \
gzip > "\$BACKUP_DIR/db_\$DATE.sql.gz"
# Upload files
tar czf "\$BACKUP_DIR/uploads_\$DATE.tar.gz" \
./strapi-app/public/uploads/
# Keep last 30 days
find "\$BACKUP_DIR" -name "*.gz" -mtime +30 -delete
echo "Backup completed: \$DATE"
Add it to cron:
# Run daily at 2 AM
0 2 * * * /opt/scripts/backup-strapi.sh >> /var/log/strapi-backup.log 2>&1
Performance Tuning
PostgreSQL Tuning
For a server with 4GB RAM dedicated to the database:
ALTER SYSTEM SET shared_buffers = '1GB';
ALTER SYSTEM SET effective_cache_size = '3GB';
ALTER SYSTEM SET work_mem = '16MB';
ALTER SYSTEM SET maintenance_work_mem = '256MB';
ALTER SYSTEM SET random_page_cost = 1.1; -- SSD storage
ALTER SYSTEM SET effective_io_concurrency = 200;
SELECT pg_reload_conf();
Strapi Caching
Enable REST API caching with the strapi-plugin-rest-cache plugin to reduce database queries by 80-90% for read-heavy APIs.
Free Resource
Free Cloud Architecture Checklist
A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.
Security Hardening
- Change default admin URL: Set
config/admin.tsto use a custom path - Rate limiting: Configure the built-in rate limiter
- CORS: Restrict origins to your frontend domains
- API tokens: Use scoped tokens instead of full-access tokens
- Disable GraphQL playground in production: Set
playgroundAlways: false
Monitoring
Add a health check endpoint and monitor it with Uptime Kuma:
# Strapi health check
curl -f http://localhost:1337/_health || exit 1
Docker Compose brings up your entire stack with a single command.
TechSaaS Deployment Pattern
We use Traefik as a reverse proxy in front of Strapi, which handles SSL termination and routes traffic based on hostname. Combined with Authelia for admin panel protection, this gives you enterprise-grade security on a self-hosted setup.
Our typical Strapi deployment serves 10,000+ API requests per minute on a 2-core, 4GB RAM allocation — far exceeding what most projects need.
Need help deploying Strapi or any headless CMS? Reach out at [email protected].
Related Service
Cloud Solutions
Let our experts help you build the right technology strategy for your business.
Need help with self-hosted?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.
We Will Build You a Demo Site — For Free
Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.
No spam. No contracts. Just a free demo.