PM2 vs Docker for running Node.js in production

PM2 and Docker solve different problems. What each one does, a side-by-side comparison, when to use one, both or neither, and a production setup for each.

"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 cluster module.
  • Zero-downtime reloads: in cluster mode, pm2 reload replaces the processes one by one, so some are always serving.
  • Start on boot: pm2 startup registers PM2 with systemd, and pm2 save remembers which apps to bring back.
  • Logs: pm2 logs collects stdout and stderr, and the pm2-logrotate module 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-stopped brings 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 once kill_timeout runs 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-stopped brings it back after a crash or a reboot.
  • --init runs a tiny init process as PID 1, which forwards signals and reaps child processes. Your app still needs to handle SIGTERM, which is what docker stop sends.
  • --memory and --cpus are hard limits, so one runaway container can't starve the rest of the server.
  • 127.0.0.1:3000 keeps 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.

Muhammad Huzaifa Abdulahad
Written by

Muhammad Huzaifa Abdulahad

Full-Stack Software & DevOps Engineer

I build SaaS platforms with Node.js, NestJS, TypeScript and PostgreSQL, and run them on Linux, Docker, AWS and CI/CD — from the first line of code to production.

Related articles

See all posts →

Have a complex idea? Let's build it together.

From MVPs to scalable products, let's turn your idea into reliable software built for real-world impact.