"Should I use PM2 or Docker?" sounds like a choice between two ways of doing the same job. It mostly isn't. PM2 is a process manager: it keeps Node.js processes running on a server. Docker packages your app with its runtime into an image that runs the same way anywhere. They overlap in one place, keeping the app alive, and that overlap is where the confusion starts.
Here's what each one actually does, how they compare, and how I'd choose.
Part of my guide to deploying Node.js to production.
TL;DR#
- PM2 runs Node.js processes on a server: restarts on crash, cluster mode across CPU cores, zero-downtime reloads, log management and start-on-boot.
- Docker packages the app, the Node.js version and the OS libraries into one image, so the build you tested is exactly what runs, on any server or orchestrator.
- On one small server with one app, PM2 alone is simple and solid.
- For CI/CD, several services, or a path to ECS or Kubernetes, use Docker.
- Running PM2 inside Docker is usually unnecessary: let Docker restart the container and run one process per container.
What PM2 does#
PM2 is a daemon that starts your Node.js processes and watches them:
- Restarts a process when it crashes, or when it passes a memory limit you set.
- Cluster mode starts one process per CPU core and balances connections between them using Node's
clustermodule. - Zero-downtime reloads: in cluster mode,
pm2 reloadreplaces the processes one by one, so some are always serving. - Start on boot:
pm2 startupregisters PM2 with systemd, andpm2 saveremembers which apps to bring back. - Logs:
pm2 logscollects stdout and stderr, and thepm2-logrotatemodule keeps them from filling the disk.
What PM2 doesn't do is package anything. It runs whatever Node.js version and system libraries are installed on that server, so two servers, or your laptop and production, can drift apart.
What Docker does#
Docker builds an image: your compiled app, its node_modules, the exact Node.js version and the OS libraries underneath, frozen together. A container is that image running.
- The same artifact everywhere. The image tested in CI is the one that runs in production, byte for byte.
- Isolation. Each container has its own filesystem and its own limits on CPU and memory.
- Restart policies.
--restart unless-stoppedbrings a crashed container back, and the Docker daemon brings containers back on boot. - Health checks that orchestrators and deploy scripts can wait on.
- The unit of deployment for Docker Compose, ECS, Kubernetes and nearly every hosting platform.
What Docker doesn't do on its own is spread one app across CPU cores, or reload it without a gap. You get those by running several containers behind a load balancer, or from an orchestrator.
Side by side#
| PM2 | Docker | |
|---|---|---|
| Restarts the app after a crash | Yes | Yes, with a restart policy |
| Starts again after a reboot | pm2 startup + pm2 save |
The daemon restarts containers |
| Uses every CPU core | Cluster mode | One container per core, or the cluster module |
| Zero-downtime reload | pm2 reload in cluster mode |
Blue-green or an orchestrator |
| Same Node.js and OS libraries everywhere | No, uses the server's | Yes, they're in the image |
| CPU and memory limits | A memory limit that triggers a restart | Enforced limits per container |
| Log rotation | pm2-logrotate module |
Log driver options |
| Moves to another server or orchestrator | Reinstall and reconfigure | Run the same image |
Running Node.js with PM2#
Install it on the server with npm install -g pm2, then describe the app in ecosystem.config.js:
module.exports = {
apps: [
{
name: 'api',
script: 'dist/main.js',
instances: 'max', // one process per CPU core
exec_mode: 'cluster',
max_memory_restart: '512M',
kill_timeout: 10000, // ms to finish open requests before being killed
env: {
NODE_ENV: 'production',
PORT: 3000,
},
},
],
};
pm2 start ecosystem.config.js
pm2 startup # prints a command to run once with sudo
pm2 save # remember the running apps for the next boot
pm2 install pm2-logrotate # rotate the log files
# On each deploy, after building the new code:
pm2 reload api
Two things catch people out:
- Graceful shutdown. PM2 stops a process with
SIGINT, then kills it oncekill_timeoutruns out. Handle the signal and close the server, or in-flight requests are cut off during every reload. PM2's signals guide covers it. - State in memory. In cluster mode each process has its own memory, so in-memory sessions, caches or lists of connected clients aren't shared. Keep them in Redis. My Server-Sent Events post shows the pattern for real-time connections.
Running Node.js with Docker#
With an image built in CI (here's a production Dockerfile), the server only needs to run it:
docker run -d --name api \
--restart unless-stopped \
--init \
--memory 512m --cpus 1 \
--env-file /srv/api/.env \
-p 127.0.0.1:3000:3000 \
ghcr.io/acme/api:$TAG
--restart unless-stoppedbrings it back after a crash or a reboot.--initruns a tiny init process as PID 1, which forwards signals and reaps child processes. Your app still needs to handleSIGTERM, which is whatdocker stopsends.--memoryand--cpusare hard limits, so one runaway container can't starve the rest of the server.127.0.0.1:3000keeps the port off the public interface; Nginx in front handles the traffic.
Also set up log rotation. Docker's default json-file log driver doesn't rotate on its own, so a chatty app slowly fills the disk. Set limits once in /etc/docker/daemon.json and restart Docker; they apply to containers created afterwards:
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
To use several CPU cores, run one container per core behind Nginx, or use Node's cluster module inside the container.
Should you run PM2 inside Docker?#
Usually not. Docker already restarts the container, and one process per container keeps logs, health checks and signals simple: when the process dies, the container stops and Docker restarts it.
The exception is a single multi-core container without an orchestrator, where you want PM2's cluster mode. PM2 ships pm2-runtime for exactly that case: it stays in the foreground the way a container's main process should. Start the container with CMD ["pm2-runtime", "ecosystem.config.js"].
How I choose#
My default is Docker, because the image CI builds and tests is exactly what runs in production, and the same image moves to ECS or Kubernetes without changes. PM2 is a good choice when it's one app on one server and the team wants the simplest thing that works; plenty of production apps run that way for years.
What I avoid is the half-way setup: building on the server, then running with PM2, so every deploy depends on whatever happens to be installed there at the time.
FAQ#
Do I need PM2 if I use Docker?#
Usually not. Docker's restart policy restarts a crashed container and brings it back after a reboot, which covers PM2's main job. Run one Node.js process per container and scale by running more containers. Use pm2-runtime only if you want cluster mode inside a single container.
Is PM2 good for production?#
Yes. PM2 is widely used to run Node.js in production: it restarts crashed processes, uses all CPU cores in cluster mode, reloads without downtime and restarts apps on boot. Its limit is that it doesn't package the runtime, so each server must have the right Node.js version and system libraries installed.
Can PM2 deploy without downtime?#
Yes, in cluster mode. pm2 reload restarts the processes one at a time, so the others keep serving requests. Handle SIGINT in your app to close the server gracefully, and set kill_timeout long enough for in-flight requests to finish.
How do I use all CPU cores with Node.js in Docker?#
Either run several containers, for example one per core, behind Nginx or another load balancer, or use Node's cluster module (or pm2-runtime in cluster mode) inside one container. An orchestrator like ECS or Kubernetes makes the first option easy to manage.
