Why self-host Strapi on a VPS
Strapi is designed to be self-hosted: it's a Node.js application that exposes a REST or GraphQL API and a React administration panel. Deploying it on your own VPS, rather than on Strapi Cloud, means your data, your uploaded media and your database stay under your exclusive control, which is often a requirement for client projects or data-localization constraints. You choose your database (PostgreSQL recommended in production over SQLite), your media storage (local disk or S3), and you are not limited by the entry or admin-user quotas of managed offerings. A VPS also lets you run Strapi and its consumer front end on the same infrastructure.
Concrete benefits
- Full ownership of the content, the PostgreSQL database and the uploaded media
- Free choice of database and storage (local disk or S3-compatible bucket)
- No quota on entries, content types or administrator users
- REST and GraphQL APIs exposed on your domain, with no intermediary or imposed rate limit
- Community plugins freely installable, without a platform's validation
- Controlled data localization, useful for compliance and client requirements
Hardware and software prerequisites
Strapi is more resource-hungry than front-end frameworks because the React admin build is heavy. Aim for a VPS with 2 vCPU and 4 GB of RAM minimum: the admin interface build can consume more than 2 GB of RAM on its own, and it's common for a build to fail with an OOM on an undersized server. In stable production, the application then runs comfortably. Install Docker and Docker Compose, and provision a PostgreSQL 16 database (ideally in its own container). Point cms.mydomain.com at the VPS IP. Plan for 15 GB of disk for the node_modules, the uploads and the backups.
Step-by-step deployment
Configure the production database
Give up SQLite in production. In config/database.js, configure the postgres client and read the credentials from the environment (DATABASE_HOST, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD). PostgreSQL is far more robust under concurrent load.
Define the production secrets
Strapi requires several keys: APP_KEYS, API_TOKEN_SALT, ADMIN_JWT_SECRET, JWT_SECRET and TRANSFER_TOKEN_SALT. Generate them randomly and place them in a .env file that is never committed. Without these keys, the admin will refuse to start in production mode.
Build the administration interface
Run NODE_ENV=production npm run build to compile the React admin panel. If the build runs out of memory, temporarily increase the VPS swap or build the Docker image on a more powerful machine before pushing it.
Orchestrate with Docker Compose
Declare a strapi service (based on node:20) and a db service (postgres:16) in docker-compose.yml, linked by an internal network. Mount a volume for public/uploads so that the media survive redeployments. Start with docker compose up -d.
Place Nginx as a reverse proxy
Configure a vhost to http://127.0.0.1:1337 (Strapi's default port). Increase client_max_body_size to 50M or more to allow large media uploads, otherwise uploads will fail with a 413 error.
Enable HTTPS and set the public URL
Run certbot --nginx -d cms.mydomain.com, then set URL=https://cms.mydomain.com in Strapi's server config. This URL is indispensable for the media links and the admin panel to work correctly behind the proxy.
Offload media storage to an S3-compatible bucket thanks to the @strapi/provider-upload-aws-s3 upload provider: your uploads no longer bloat the VPS disk and survive any container recreation, with no volume to back up. For the database, automate a daily pg_dump via a cron on the VPS and copy the dump off the server. Finally, run Strapi with NODE_ENV=production without fail: development mode recompiles the admin on the fly and exposes the Content-Type Builder, to be avoided in production.