[{"data":1,"prerenderedAt":169},["ShallowReactive",2],{"seo-verification":3,"blog-deploying-a-strapi-application-on-a-vps-en":6},{"google":4,"bing":5},"EycwPY2XMyTkVzas3n1ygeNJFGAH513qrMjfDljzsMQ","",{"id":7,"slug":8,"slugs":9,"title":12,"excerpt":13,"readTime":14,"views":15,"isPinned":16,"publishedAt":17,"category":18,"categories":24,"featuredImage":26,"bgImage":27,"posterImage":28,"relatedSolution":26,"intro":29,"sections":30,"ctaTitle":117,"ctaBody":118,"ctaButton":119,"ctaUrl":120,"relatedPosts":121},54,"deploying-a-strapi-application-on-a-vps",{"fr":10,"en":8,"ar":11},"deployer-strapi-vps","نشر-تطبيق-strapi-على-خادم-vps","Deploy Strapi on a VPS: the complete guide","Deploy Strapi on a VPS with Docker, PostgreSQL and Nginx: multi-stage Dockerfile, secrets, S3, pg_dump backups and OOM troubleshooting.",12,0,false,"2026-04-27T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},4,"Development","developpement","bg-warning\u002F10 text-warning","dev",[25],{"id":19,"name":20,"slug":21,"color":22,"icon":23},null,"\u002Fblog\u002Fcovers\u002Fbg.svg","\u002Fblog\u002Fcovers\u002Fdeployer-strapi-vps-poster.svg","Strapi is the reference open source headless CMS in the Node.js ecosystem. Self-hosting it on a VPS gives you full ownership of your content, your database and your uploads — with no subscription quota, no dependency on Strapi Cloud, and no third-party platform standing between your teams and your data. This guide covers the whole deployment: project generation, multi-stage Dockerfile, docker-compose with PostgreSQL, production secrets, Nginx, HTTPS, S3 storage, automated backups and upgrades that do not break.",[31,35,47,50,66,84,87,105,108,111,114],{"type":32,"title":33,"body":34},"h2","Strapi Cloud or self-hosting: what you lose, what you gain","Strapi Cloud is convenient to get started, but it comes with constraints that quickly become blocking in real production. The free tier limits content types and admin users; paid tiers bill on the number of API requests and uploads. Above all, your data lives on Strapi Inc.'s infrastructure — which is incompatible with the data localisation requirements many clients have (strict GDPR, sector-specific contracts, sensitive data). By self-hosting on a VPS, you regain full ownership of the PostgreSQL database, the choice of storage (local disk or S3-compatible bucket), the freedom to install any community plugin, and content volumes that no longer depend on a pricing grid. The trade-off is operational — you handle upgrades, backups and monitoring. This guide shows you exactly how.",{"type":36,"title":37,"items":38},"ul","Benefits of self-hosting Strapi on a VPS",[39,40,41,42,43,44,45,46],"**Data ownership** — your PostgreSQL database and your uploads stay on your VPS, under your exclusive control","**No plan quotas** — the number of entries, content types and admin accounts is no longer set by a subscription, but by the resources of your VPS","**Community plugins** — any npm plugin installs freely, with no platform review or allowlist","**API on your own domain** — REST and GraphQL exposed on `cms.your-domain.com`, with no intermediary and no imposed rate limit","**Unified stack** — Strapi and a Nuxt or Next.js frontend live on the same VPS, behind a single reverse proxy","**Data localisation** — essential for GDPR compliance and for your clients' contractual requirements","**Predictable cost** — the VPS has a fixed monthly rate, independent of content volume or API traffic","**Migration control** — you decide when and how to apply Strapi upgrades",{"type":32,"title":48,"body":49},"Prerequisites: hardware, software and domain","Strapi is hungrier than most frameworks because the **React admin build alone consumes more than 2 GB of RAM**. A VPS with 1 GB of RAM will systematically die of OOM during the build phase — do not undersize it. Minimum targets: **2 vCPU, 4 GB of RAM**. In steady production, once the build is done, the application runs comfortably on 512 MB to 1 GB. On the software side: Docker 24+ and Docker Compose v2 (`docker compose`, not `docker-compose`), Git to pull your code onto the server, and Certbot for TLS. On the database side: PostgreSQL 16 is recommended in production — SQLite works in development but is too limited under concurrent load and incompatible with some plugins. On the network side: point `cms.your-domain.com` at your VPS IP before you start, and plan for at least 20 GB of disk (node_modules, uploads, backup dumps).",{"type":51,"title":52,"steps":53},"steps","Prepare the project before touching the server",[54,57,60,63],{"title":55,"body":56},"Generate the Strapi project","On your workstation, generate the project with the official command `npx create-strapi-app@latest my-cms --dbclient=postgres`. Choose TypeScript if your team is comfortable with it. Initialise the Git repository, push it to your forge, then clone it on the VPS into `\u002Fsrv\u002Fmy-cms`. Everything that follows is prepared in that repository and not on the server: the VPS should only ever receive code that is already versioned.",{"title":58,"body":59},"Wire the database configuration to PostgreSQL","In `config\u002Fdatabase.js` — or `config\u002Fdatabase.ts` in TypeScript — the exported function receives `env` and returns a `connection` object. Declare `client: 'postgres'`, then six keys read from the environment inside the `connection` sub-object: `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', '')` and `ssl: env.bool('DATABASE_SSL', false)`. The second argument of `env()` is only a development fallback: no real value belongs in this file, it goes into Git.",{"title":61,"body":62},"Generate the five production secrets","Strapi refuses to start in production without `APP_KEYS`, `API_TOKEN_SALT`, `ADMIN_JWT_SECRET`, `JWT_SECRET` and `TRANSFER_TOKEN_SALT`. Generate each one with `openssl rand -base64 32`; `APP_KEYS` expects several, separated by commas, so produce at least two. Write them into `\u002Fsrv\u002Fmy-cms\u002F.env` on the VPS — never into Git — together with `DATABASE_HOST`, `DATABASE_NAME`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `NODE_ENV=production` and `URL=https:\u002F\u002Fcms.your-domain.com`. A missing key means a refusal to start, not a warning.",{"title":64,"body":65},"Write the multi-stage Dockerfile","Two stages are enough to keep the production image light. The `builder` stage starts from `node:20-alpine`, sets `WORKDIR \u002Fapp`, copies `package*.json`, runs `npm ci`, copies the rest of the code, then compiles the admin with `NODE_ENV=production npm run build`. The `runner` stage starts again from the same base image and copies only three things from `builder`: the build folder, `node_modules` and `package.json`. Finish with `EXPOSE 1337` and a `CMD` that calls `npm run start` — that is the start script Strapi installs, do not try to launch a server file by hand. If the build dies of OOM, set `NODE_OPTIONS=--max-old-space-size=4096` before `npm run build`.",{"type":51,"title":67,"steps":68},"Deploy the stack on the VPS",[69,72,75,78,81],{"title":70,"body":71},"Describe the services in docker-compose.yml","The file declares two services and one named volume. The `postgres` service uses the `postgres:16-alpine` image, receives `POSTGRES_DB`, `POSTGRES_USER` and `POSTGRES_PASSWORD` in its `environment` block, mounts the `pgdata` volume on `\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata` and runs with `restart: unless-stopped`. The `strapi` service builds from the local Dockerfile with `build: .`, reads its variables through `env_file: .env`, declares `depends_on: postgres`, mounts `.\u002Fpublic\u002Fuploads` on `\u002Fapp\u002Fpublic\u002Fuploads` and publishes its port with `ports: 127.0.0.1:1337:1337`. That binding on the loopback interface is the important point: Nginx becomes the only public entry point, and port 1337 is never exposed to the outside. A `redis` service remains optional, for session caching or processing queues.",{"title":73,"body":74},"Build the image and start the stack","Run `docker compose build`, then `docker compose up -d`. Follow the startup with `docker compose logs -f strapi`. Strapi applies its schema migrations on first start as soon as `NODE_ENV=production` is set. Expect two to five minutes: building the React admin is by far the slowest step. Wait for the `Strapi started successfully` line before going further.",{"title":76,"body":77},"Put Nginx in front as a reverse proxy","Create the vhost `\u002Fetc\u002Fnginx\u002Fsites-available\u002Fcms.your-domain.com`. It listens with `listen 80` on `server_name cms.your-domain.com`, carries a `client_max_body_size 50M` — indispensable for media uploads — and a single `location \u002F` block that does `proxy_pass http:\u002F\u002F127.0.0.1:1337`. Add the four headers Strapi expects behind a proxy: `Host`, `X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto`, each set by a `proxy_set_header` directive. Enable the vhost with a symbolic link into `sites-enabled`, then validate and reload with `nginx -t && systemctl reload nginx`.",{"title":79,"body":80},"Enable HTTPS and set the URL variable","Obtain the certificate with `certbot --nginx -d cms.your-domain.com`: Certbot rewrites the vhost to redirect HTTP to HTTPS. Then check that `URL=https:\u002F\u002Fcms.your-domain.com` really is in the `.env`, with no trailing slash. That variable is what Strapi uses to build media links and admin redirects; without it, uploaded files come out with wrong addresses and the admin panel misbehaves behind the proxy. Restart the container after any change to the `.env`: variables are read at startup.",{"title":82,"body":83},"Check that the instance really answers","Open `https:\u002F\u002Fcms.your-domain.com\u002Fadmin` and create the first administrator account — Strapi asks for it on first access and will not ask again. Then check three things: `docker compose ps` shows both containers in the `running` state, the public API answers on `\u002Fapi`, and uploading a test file from the media library succeeds. A `502 Bad Gateway` at this stage almost always means a stopped `strapi` container, or one still building: read `docker compose logs strapi` before touching Nginx.",{"type":32,"title":85,"body":86},"Moving media off the disk: the S3 provider","By default, Strapi stores uploaded files in `public\u002Fuploads`, on the VPS disk. The `.\u002Fpublic\u002Fuploads` mount in docker-compose lets them survive a container recreation, but they grow with the media library, end up in every backup and disappear with the server. The official provider solves all three problems: `npm install @strapi\u002Fprovider-upload-aws-s3`. Then declare it in `config\u002Fplugins.js`, under the `upload` key and then `config`: `provider: 'aws-s3'` and a `providerOptions` object that reads four values from the environment — `accessKeyId: env('AWS_ACCESS_KEY_ID')`, `secretAccessKey: env('AWS_ACCESS_SECRET')`, `region: env('AWS_REGION')` and `params: { Bucket: env('AWS_BUCKET') }`. Any S3-compatible storage will do: Scaleway Object Storage, Wasabi, Cloudflare R2. Two pitfalls are worth knowing before you switch. Already uploaded media do not migrate on their own — switch the provider before going live, or copy the contents of `public\u002Fuploads` into the bucket by hand. And media URLs change domain: if your frontend caches or rewrites them, check that after the switch.",{"type":51,"title":88,"steps":89},"Automate the database backup",[90,93,96,99,102],{"title":91,"body":92},"Create the destination folder","On the VPS, `mkdir -p \u002Fsrv\u002Fbackups\u002Fstrapi`. Keep this folder outside the Git repository and outside any path mounted into a container: a dump should never end up in an image or in a release.",{"title":94,"body":95},"Find the real name of the PostgreSQL container","`docker compose ps` gives the exact name, of the form `my-cms-postgres-1`. It derives from the project folder name: do not copy it from a guide, read it on your own machine. A backup script aimed at a container that does not exist fails silently once it is in the cron.",{"title":97,"body":98},"Write the dump command","A single line is enough: `docker exec my-cms-postgres-1 pg_dump -U strapi strapi | gzip > \u002Fsrv\u002Fbackups\u002Fstrapi\u002Fstrapi-$(date +%Y%m%d).sql.gz`. The `pg_dump` runs inside the container, compression and writing happen on the host. Run it once by hand and check the size of the file produced: a dump of a few bytes signals an authentication error swallowed by the pipe.",{"title":100,"body":101},"Purge dumps that are too old","Add `find \u002Fsrv\u002Fbackups\u002Fstrapi -name '*.sql.gz' -mtime +7 -delete` right after. Without that line, the disk fills up within a few weeks: it is the most mundane failure of a daily backup — the database goes down because the backup saturated the volume.",{"title":103,"body":104},"Schedule it, then move the dumps off the server","Put both commands in a `\u002Fetc\u002Fcron.daily\u002Fstrapi-backup` script, with `#!\u002Fbin\u002Fbash` on the first line and a `chmod +x` to make it executable. Add an `rsync` to external storage, or a copy to the same S3 bucket as the media. A backup that stays on the machine it backs up protects nothing; test a full restore at least once before relying on it.",{"type":32,"title":106,"body":107},"Upgrade Strapi without breaking the migrations","An upgrade is driven from the repository, not on the server: change the version in `package.json`, push, then, on the VPS, chain `git pull` and `docker compose build && docker compose up -d`. Strapi detects and applies schema migrations at startup as long as `NODE_ENV=production` is set; confirm it with `docker compose logs strapi | grep -i migrat`. Three precautions are worth the detour. Take a `pg_dump` right before the version bump, and not just the one from the previous night — a failed migration is repaired by a restore, never by a second attempt. Read the release notes of the plugins you have installed: a major Strapi bump breaks a community plugin more often than the core itself. Finally, if a migration fails, do not restart the stack in a loop: every restart replays the same migration on a database that is already half modified. Stop the containers, read the full log, restore the dump if needed, then fix.",{"type":32,"title":109,"body":110},"Troubleshooting: the most frequent errors","**OOM during the build (`Killed` or `JavaScript heap out of memory`)**: the React admin build exceeds the available memory. Two remedies — add `NODE_OPTIONS=--max-old-space-size=4096` to the `builder` stage of the Dockerfile, or temporarily increase the VPS swap with `fallocate -l 2G \u002Fswapfile`, `chmod 600 \u002Fswapfile`, `mkswap \u002Fswapfile` then `swapon \u002Fswapfile`. If the VPS still gets stuck, build the image on a more powerful machine and push it to a registry.\n\n**`Cannot find module @strapi\u002Fplugin-*`**: never share the `node_modules` folder between a local development environment and the production image through a Docker volume. The local `node_modules` is compiled for your operating system, not for the container's Alpine Linux. Remove any `node_modules` volume from the docker-compose and let the `npm ci` in the Dockerfile handle it.\n\n**`URL mismatch` in the admin, or media on relative paths**: the `URL` variable in the `.env` must match the public HTTPS address of your Strapi exactly, with no trailing slash. Any divergence breaks the links of uploaded media and causes CORS errors in the admin panel.\n\n**`413 Request Entity Too Large`**: the Nginx `client_max_body_size` directive is too low. Raise it to `50M` at least, or to `100M` if you upload videos, then reload Nginx.\n\n**`password authentication failed for user` at startup**: the password in the `.env` changed after the `pgdata` volume was created. PostgreSQL only reads its password at volume initialisation; align the `.env` with the existing password, or start from a fresh volume after backing up the database.",{"type":112,"body":113},"tip","Three non-negotiable rules for a Strapi instance in production: **S3 for uploads** (`@strapi\u002Fprovider-upload-aws-s3`) — your media survive any container recreation and do not inflate the VPS disk; **automated daily `pg_dump`** copied off the server — a local-only backup disappears with the VPS; **`NODE_ENV=production` without exception** — development mode recompiles the admin on the fly, exposes the Content-Type Builder and disables cache optimisations, all of which are to be avoided in production.",{"type":32,"title":115,"body":116},"Strapi and a Nuxt or Next.js frontend on the same VPS","It is perfectly possible to run Strapi and a frontend side by side on the same VPS, provided you have enough RAM — count on 8 GB for both, since the two builds can be triggered at the same time. The simplest architecture uses Nginx as a dispatcher: requests to `cms.your-domain.com` are proxied to port `1337` (Strapi), and those to `your-domain.com` to port `3000` (Nuxt or Next.js). Strapi exposes its REST API on `\u002Fapi` — the frontend consumes it directly over the internal Docker network, without going back through Nginx, which reduces latency. If your frontend generates static pages (`nuxt generate`, or `next build` in static export), Nginx can serve the files from disk and proxy only the dynamic routes. This monolithic architecture is well suited to a medium-sized project — one server, one TLS certificate, one supervision point. If you would rather separate the frontend, our Nuxt and Node.js deployment guides apply the same method on a second VPS sized for the build.","Host your Strapi CMS with full autonomy","The ServOrbit Cloud VPS provides the RAM required for the Strapi admin build and a ready-to-use Docker + PostgreSQL environment, to keep full ownership of your content and your data.","Discover the Cloud VPS","\u002Fvps-cloud",[122,137,154],{"id":123,"slug":124,"slugs":125,"title":128,"excerpt":129,"readTime":19,"views":15,"isPinned":16,"publishedAt":130,"category":131,"categories":132,"featuredImage":26,"bgImage":27,"posterImage":134,"relatedSolution":135},5,"deploying-laravel-on-a-vps-a-production-guide",{"fr":126,"en":124,"ar":127},"deployer-laravel-vps","نشر-laravel-على-خادم-vps-دليل-الإنتاج","Deploying Laravel on a VPS: a production guide","Take Laravel to production on a VPS: PHP, workers, cache, database, reverse proxy and HTTPS configured cleanly.","2026-02-09T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},[133],{"id":19,"name":20,"slug":21,"color":22,"icon":23},"\u002Fblog\u002Fcovers\u002Fdeployer-laravel-vps-poster.svg",{"categorySlug":23,"appSlug":136},"laravel-stack",{"id":138,"slug":139,"slugs":140,"title":143,"excerpt":144,"readTime":145,"views":15,"isPinned":16,"publishedAt":146,"category":147,"categories":148,"featuredImage":26,"bgImage":27,"posterImage":150,"relatedSolution":151},43,"deploy-a-nodejs-application-on-a-vps",{"fr":141,"en":139,"ar":142},"deployer-nodejs-vps","نشر-تطبيق-nodejs-على-vps","Deploy a Node.js Application on a VPS","Deploy a Node.js application to production on a VPS: PM2, Nginx reverse proxy, Let's Encrypt SSL, and automatic startup at boot.",3,"2026-05-08T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},[149],{"id":19,"name":20,"slug":21,"color":22,"icon":23},"\u002Fblog\u002Fcovers\u002Fdeployer-nodejs-vps-poster.svg",{"categorySlug":152,"appSlug":153},"development","nodejs-stack",{"id":155,"slug":156,"slugs":157,"title":160,"excerpt":161,"readTime":145,"views":15,"isPinned":16,"publishedAt":162,"category":163,"categories":164,"featuredImage":26,"bgImage":27,"posterImage":166,"relatedSolution":167},44,"deploy-a-django-application-on-a-vps",{"fr":158,"en":156,"ar":159},"deployer-django-vps","نشر-تطبيق-django-على-vps","Deploy a Django Application on a VPS","Deploy Django on a VPS: Gunicorn, Nginx, PostgreSQL, Docker, and SSL. A complete guide for Python developers who want to self-host.","2026-05-07T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},[165],{"id":19,"name":20,"slug":21,"color":22,"icon":23},"\u002Fblog\u002Fcovers\u002Fdeployer-django-vps-poster.svg",{"categorySlug":152,"appSlug":168},"django",1787580996551]