npm run start runs Strapi in the foreground. Close the SSH session and the site goes down. Crash the process and nothing brings it back. Reboot the server and you are logging in to start it by hand.
A process manager fixes all three. PM2 is the usual choice for Node applications, and the Strapi docs recommend it for self-hosted deployments.
Install
npm install pm2 -g
# or
yarn global add pm2Three ways to start Strapi under PM2
1. With a server.js file
Create server.js at the project root:
const strapi = require('@strapi/strapi');
strapi.createStrapi().start();pm2 start server.js --name strapiTypeScript projects need distDir. A TypeScript project compiles to ./dist, and the factory has to be pointed at it:
const strapi = require('@strapi/strapi');
strapi.createStrapi({ distDir: './dist' }).start();Without that, PM2 starts a process that loads no application code and exits or serves nothing.
Upgrading from Strapi v4: v4 used
strapi({ distDir: './dist' }).start(). Strapi 5 exposes the factory ascreateStrapi. A copied v4server.jswill throwstrapi is not a function.
2. With the strapi command
No extra file needed:
pm2 start npm --name strapi -- run startor:
pm2 start "npx strapi start" --name strapiSimple, and it goes through your package.json scripts, so whatever npm run start does locally is what runs in production.
3. With an ecosystem file (recommended)
This is the version worth using for anything real, because the configuration lives in the repository rather than in shell history.
module.exports = {
apps: [
{
name: 'strapi',
cwd: '/var/www/strapi',
script: 'npm',
args: 'run start',
instances: 1,
exec_mode: 'fork',
autorestart: true,
max_memory_restart: '1G',
watch: false,
env: {
NODE_ENV: 'production',
HOST: '0.0.0.0',
PORT: 1337,
PUBLIC_URL: 'https://cms.example.com',
},
error_file: '/var/log/strapi/error.log',
out_file: '/var/log/strapi/out.log',
merge_logs: true,
time: true,
},
],
};pm2 start ecosystem.config.jsNote exec_mode: 'fork' and instances: 1. This matters, and the next section explains why.
Do not use PM2 cluster mode without reading this
PM2's cluster mode forks multiple Node processes sharing a port. It is the default advice for scaling Node apps and it is a trap for Strapi.
Each Strapi instance loads the schema, runs migrations, and performs a database schema sync at startup. Several instances doing that simultaneously against the same database race each other, and the failure modes range from duplicate migration attempts to a corrupted schema state.
Beyond boot, cluster mode also breaks:
- Local file uploads: each worker writes to its own view of
public/uploads, and requests are load-balanced, so uploads appear to vanish. Use S3, Cloudinary, or another shared provider. - In-memory caching: not shared between workers.
- Cron jobs: every worker runs them, so a nightly job runs N times.
If you need to run more than one instance:
- Use a shared upload provider.
- Run migrations once, as a separate deploy step, before starting any instance.
- Run cron on exactly one instance, gated behind an environment variable.
- Prefer separate PM2 apps on distinct ports behind Nginx over cluster mode, so each instance is individually addressable.
Realistically, most Strapi deployments run one instance per machine and scale horizontally across machines.
Secrets do not belong in the ecosystem file
The env block above shows non-secret values. Do not put DATABASE_PASSWORD, APP_KEYS, ADMIN_JWT_SECRET, JWT_SECRET, API_TOKEN_SALT, or ENCRYPTION_KEY in a file you commit.
Keep them in .env on the server (readable only by the service user), and PM2 will pick them up because Strapi loads it, or use env_file:
module.exports = {
apps: [
{
name: 'strapi',
cwd: '/var/www/strapi',
script: 'npm',
args: 'run start',
env_file: '/var/www/strapi/.env',
env: {
NODE_ENV: 'production',
},
},
],
};chmod 600 /var/www/strapi/.env
chown strapi:strapi /var/www/strapi/.envSurviving a reboot
Two separate steps, and people routinely do only the first:
# 1. Generate and install the systemd/launchd startup script
pm2 startup
# Run the command it prints, which needs sudo
# 2. Save the current process list as the one to resurrect
pm2 saveWithout pm2 save, PM2 starts on boot with an empty process list. Re-run pm2 save whenever you add or change an app.
Deploying an update
The order matters. Build before restarting, or you will serve a half-updated admin panel:
cd /var/www/strapi
git pull
npm ci
NODE_ENV=production npm run build
pm2 restart strapi --update-env--update-env re-reads environment variables. Without it, PM2 restarts the process with the environment it captured the first time, which is a genuinely baffling bug to chase when you have just changed a variable.
For zero-downtime updates you need two instances behind Nginx and pm2 reload; with a single instance, restart means a few seconds of 502s. If that matters, drain the instance from the load balancer first.
Logs
pm2 logs strapi # tail
pm2 logs strapi --lines 200 # recent history
pm2 flush # clearStrapi is chatty. Install log rotation on day one, before the disk fills:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress trueMonitoring
pm2 status # process list
pm2 monit # live CPU and memory
pm2 describe strapi # full detail, including restart countA climbing restart count in pm2 describe means the process is crash-looping. Check pm2 logs for the reason; it is usually a database connection failure or a missing environment variable at boot.
Pair PM2 with Strapi's own health endpoint for external monitoring:
curl -I https://cms.example.com/_health # → 204max_memory_restart is a bandage
max_memory_restart: '1G' restarts the process when it exceeds a gigabyte. It keeps a leaking service available, and it hides the leak. If it fires regularly, find out why: large image processing with sharp and unbounded query results are the two usual causes in Strapi.
Command reference
| Task | Command |
|---|---|
| Start | pm2 start ecosystem.config.js |
| Stop | pm2 stop strapi |
| Restart with fresh env | pm2 restart strapi --update-env |
| Reload (zero-downtime, needs >1 instance) | pm2 reload strapi |
| Remove from PM2 | pm2 delete strapi |
| Status | pm2 status |
| Logs | pm2 logs strapi |
| Persist across reboot | pm2 startup then pm2 save |
What about Docker?
If you are already running Strapi in a container, you do not need PM2. The container runtime, whether Docker's restart policy, Kubernetes, or ECS, is your process manager. Adding PM2 inside a container gives you two supervisors disagreeing about restarts and hides crashes from the orchestrator.
Run Node directly as PID 1 in the container and let the platform handle restarts.




