Strapi Self-Hosted Setup
Self-hosting Strapi trades vendor convenience for control over data residency, plugins, and deployment — and for the operational overhead that comes with owning all three. This guide is the production blueprint: runtime prerequisites, environment-driven config, perimeter controls, and containerized deployment, within Platform Integration Deep Dives.
Architecture & Runtime Prerequisites
Strapi v5 needs an active Node.js LTS — v20 or v22 (v18 is end-of-life and unsupported), per the Node.js LTS schedule, a relational database, and optionally Redis for cache and sessions. Use PostgreSQL: its JSONB support and concurrent-write handling directly raise API throughput under heavy ingestion (see the PostgreSQL JSONB docs for indexing complex content). The build decouples admin from API: /admin compiles to static assets while the Node process serves REST/GraphQL — the same UI/delivery split as Sanity Studio Customization.
The self-hosted topology puts a hardened perimeter in front of the Strapi process, which serves both the static admin and the API off one database:
flowchart LR FE["Frontend domains"] --> Proxy["Reverse proxy (NGINX / Traefik / Cloudflare)"] Proxy -->|"CORS + rate limit"| Strapi["Strapi (Node process)"] Strapi --> Admin["/admin (static assets)"] Strapi --> Api["REST / GraphQL API"] Strapi --> DB["PostgreSQL"] Strapi -.-> Redis["Redis (cache / sessions)"]
Environment Configuration & Schema Management
Configuration must be strictly environment-driven. Inject variables via your orchestrator or CI/CD pipeline rather than committing .env files to version control.
NODE_ENV=production
DATABASE_CLIENT=postgres
DATABASE_HOST=db.internal
DATABASE_PORT=5432
DATABASE_NAME=strapi_prod
DATABASE_USERNAME=strapi_user
DATABASE_PASSWORD=${DB_SECRET}
JWT_SECRET=${JWT_SECRET}
ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
APP_KEYS=${APP_KEYS}
Schema sync is explicit: Strapi compiles assets with strapi build and runs with strapi start. Automate that sequence in CI or staging and production drift apart. Teams coming from a managed platform can compare against the Contentful Integration Guide to see how explicit schema management differs from a vendor-locked model and how to map existing schemas onto Strapi content types.
Security & Perimeter Controls
A public headless CMS needs a hard perimeter. Set CORS in config/middlewares.js to allow only your frontend domains, and rate-limit at the reverse proxy (NGINX, Traefik, Cloudflare) or via strapi-plugin-ratelimit to blunt credential stuffing on the auth endpoints.
Strapi’s permissions engine needs careful scoping or it leaks data. Strapi Role-Based Access Control Configuration covers least-privilege editing, API-token scopes, and publication workflows. Don’t use Super Admin for routine content work — map roles to specific content types with restricted lifecycle actions (create, update, publish).
Containerization & Deployment Patterns
Containers give the cleanest dev-to-prod parity. A minimal docker-compose.yml for local and staging (Docker Compose specification):
version: '3.8'
services:
strapi:
build: .
environment:
- NODE_ENV=production
- DATABASE_CLIENT=postgres
- DATABASE_HOST=db
- DATABASE_PORT=5432
- DATABASE_NAME=${DATABASE_NAME}
- DATABASE_USERNAME=${DATABASE_USERNAME}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- JWT_SECRET=${JWT_SECRET}
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
- APP_KEYS=${APP_KEYS}
ports:
- "1337:1337"
depends_on:
- db
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=${DATABASE_USERNAME}
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
- POSTGRES_DB=${DATABASE_NAME}
volumes:
- strapi-data:/var/lib/postgresql/data
volumes:
strapi-data:
For production, orchestrate via Kubernetes or ECS. The Self-hosting Strapi on AWS for enterprise apps blueprint covers IaC templates, auto-scaling, RDS Proxy, and the connection-pool tuning that prevents the most common production failure. Have CI run npm ci, npm run build, and a health check against /admin and /api before promoting the image.