PM2, Docker or Kubernetes for Node?
After 79 days of building NestJS applications, you've finally got something production-ready. Now comes the real question: how do you actually run it? The answer depends entirely on your team size, infrastructure budget, and operational maturity. I've deployed Node services across all three models, and I can tell you there's no one-size-fits-all solution—only the right fit for where you are today.
PM2 on a VM: Simple, Cheap, Great for Small Teams
PM2 is the first approach I recommend for teams of 1-5 developers or early-stage startups. It's a process manager that keeps your Node application running on a single Linux box, restarts it on crashes, and handles log rotation. You get simplicity and low cost without orchestration complexity.
Here's what a typical PM2 setup looks like. You SSH into an Ubuntu VM (probably a t3.medium on AWS or equivalent), install Node, and then create an ecosystem.config.js file:
module.exports = {
apps: [
{
name: 'api',
script: './dist/main.js',
instances: 4,
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
DATABASE_URL: process.env.DATABASE_URL,
REDIS_URL: process.env.REDIS_URL,
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
max_memory_restart: '500M',
},
],
};
Then you run pm2 start ecosystem.config.js and it's live. PM2 handles clustering across CPU cores, zero-downtime reloads, and automatic restarts if your process crashes. You can monitor it via the CLI or their paid dashboard if you want deeper observability.
The real win here is operational simplicity. Your CI/CD pipeline is just: build, upload tarball, extract, run PM2 restart. No Docker builds, no container registry, no orchestration learning curve. Costs are minimal—a single $20/month VM can run multiple small APIs. I've kept small production services alive on PM2 for years with minimal maintenance.
The trade-off is clear: you're betting on a single machine. If it goes down, you have downtime. If you outgrow one box, scaling becomes messy. And reproducing the production environment locally requires matching OS, libraries, and environment variables perfectly—which almost never happens.
Docker Compose: Reproducible Environments
Docker Compose bridges the gap between local development and production. It lets you define your entire stack—API, database, cache, message queue—in a single docker-compose.yml file that works identically on your laptop and in production.
Here's a realistic example for a NestJS application:
version: '3.9'
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000'
environment:
DATABASE_URL: postgresql://user:pass@postgres:5432/mydb
REDIS_URL: redis://redis:6379
NODE_ENV: production
depends_on:
- postgres
- redis
restart: unless-stopped
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Your Dockerfile is equally straightforward:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
EXPOSE 3000
CMD ["node", "dist/main.js"]
Now every developer runs docker-compose up and gets the exact same environment. No "works on my machine" arguments. Your CI/CD builds and pushes images to a registry (ECR, Docker Hub, etc.), and your production server pulls and runs them with docker-compose pull && docker-compose up -d.
Docker Compose works beautifully for small-to-medium deployments: teams of 5-15 people, 1-3 services, single or dual server deployment. You get reproducibility, versioned images, and still reasonable operational overhead. I've run this pattern successfully with ride-hailing platforms serving millions of requests monthly.
The limitation arrives when you need true high availability, multi-region failover, or scaling services independently. Docker Compose is great at orchestrating services on a single host, but managing multiple hosts manually becomes error-prone.
Kubernetes: Many Services and Autoscaling
Kubernetes is the orchestration platform for teams running dozens of microservices, needing automatic failover, and wanting to optimize infrastructure costs through intelligent resource allocation. It abstracts away the underlying servers and lets you declare desired state—then it makes it happen.
A basic NestJS deployment on Kubernetes looks like this. First, your Deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
labels:
app: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myregistry.azurecr.io/api:v1.2.3
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: NODE_ENV
value: production
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: '256Mi'
cpu: '250m'
limits:
memory: '512Mi'
cpu: '500m'
Add a Service to expose it and an Ingress for external traffic, then Kubernetes handles pod scheduling, rolling updates, restarts, and networking automatically. Add a Horizontal Pod Autoscaler and your API scales from 3 to 10 replicas based on CPU usage during traffic spikes—all without manual intervention.
Kubernetes shines when you have 10+ microservices, teams of 20+ engineers, complex networking requirements, or multi-region deployments. It pays for itself through efficient resource utilization, automated failover, and the ability to update services without downtime.
I've deployed to Kubernetes for large e-commerce platforms, SaaS products, and fintech trading systems. Once your team understands the mental model—declarative state, controllers, operators—it becomes incredibly powerful. But that learning curve is real, and the operational overhead is significant.
Do Not Adopt Kubernetes for a Single API
This deserves its own section because I see it constantly: startups with one API service adopting Kubernetes because "we might scale later." You won't scale later by accident. Scaling is a deliberate choice driven by revenue and users. Premature Kubernetes adoption taxes your velocity with minimal benefit.
A single API on Kubernetes still requires you to understand pod lifecycle, resource requests, networking policies, persistent volumes, and secret management. Your deployment becomes opaque—a pod fails and you're SSH-ing into nodes debugging kubelet logs instead of shipping features. Your CI/CD pipeline needs a container registry, helm charts or kustomize, and cluster access credentials. Your on-call engineer needs a $300 Kubernetes course.
The cost-benefit is inverted. You spend 2-3 months learning Kubernetes to solve problems you don't have yet. Instead, start with PM2 or Docker Compose. When you genuinely need Kubernetes—when you have 5+ services and your team is fighting coordination problems—then migrate. The patterns you learn building for Docker Compose transfer directly to Kubernetes.
Common Mistakes
- Choosing deployment model before defining the problem: Don't pick Kubernetes because it's trendy. Define: How many services? How many people managing infrastructure? What's your failover requirement? Answer those first.
- Skipping health checks: All three approaches work better with proper health endpoints. A crashed process looks alive until it doesn't respond to traffic. Build
/healthfrom day one. - Coupling your code to deployment strategy: Write your NestJS app to run anywhere. Don't hardcode file paths, assume local sockets, or depend on container orchestration features.
- Setting resource limits too tight: A process that hits its memory limit gets killed instantly. Be generous initially, then tune after measuring real usage.
- Migrating without a rollback plan: Moving from PM2 to Docker Compose is easy only if you keep PM2 running on a parallel server during transition. Plan your switchover.
Key Takeaways
- PM2 on a VM: best for small teams, single services, low budget. Zero container overhead, simple mental model.
- Docker Compose: bridges development and production, reproducible environments, works for 1-3 services on 1-2 servers.
- Kubernetes: choose this only when you have multiple services, high availability needs, and enough team maturity to maintain it.
- Match deployment model to team size and problem complexity, not aspirational scale.
- Every approach requires health checks, proper logging, and configuration management. Don't skip those regardless of choice.
- Build portable code that doesn't depend on deployment specifics—you'll migrate eventually and regret it otherwise.






Comments
Loading comments…