← All articlesSelf-Hosted

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.

Y
Yash Pritwani
14 min read

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.

ProductionWeb ServerApp ServerDatabaseMonitoringStagingWeb ServerApp ServerDatabaseVLANBackupStorage3-2-1 Rule

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$5,000/moMigrateBare MetalDocker + LXC$200/mo96% cost reduction

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.

Download the Checklist

Security Hardening

  1. Change default admin URL: Set config/admin.ts to use a custom path
  2. Rate limiting: Configure the built-in rate limiter
  3. CORS: Restrict origins to your frontend domains
  4. API tokens: Use scoped tokens instead of full-access tokens
  5. 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
Terminal$docker compose up -d[+] Running 5/5Network app_default CreatedContainer web StartedContainer api StartedContainer db Started$

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].

#strapi#headless-cms#self-hosted#postgresql#docker

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.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.