Deployment Options
| Platform | Best For | Effort | Scaling | Cost |
|---|
| Heroku | Quick deployment, startups | Low | Manual/Auto | Pay-per-dyno |
| Docker | Containerized apps, consistency | Medium | Orchestration | Depends on host |
| VPS (DigitalOcean, AWS EC2) | Full control, custom setup | High | Manual | Fixed monthly |
Pre-Deployment Checklist
Production-Ready Server Setup
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cors = require('cors');
const app = express();
app.use(helmet());
app.use(compression());
app.use(cors({ origin: process.env.CLIENT_URL }));
app.use(express.json({ limit: '100kb' }));
app.set('trust proxy', 1);
app.use('/api', require('./routes'));
app.use((req, res) => res.status(404).json({ error: 'Not found' }));
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
1. Deploying to Heroku
Requirements
heroku login
heroku create my-express-app
Procfile
web: npm start
package.json Scripts
{
"scripts": {
"start": "node index.js",
"heroku-postbuild": "echo 'Build complete'"
},
"engines": {
"node": "18.x"
}
}
Deploy
heroku config:set NODE_ENV=production
heroku config:set JWT_SECRET=$(openssl rand -base64 32)
heroku config:set MONGO_URI=mongodb+srv://...
heroku addons:create mongolab:sandbox
git push heroku main
heroku open
heroku logs --tail
heroku ps:scale web=1
Zero-Downtime Deployment
heroku ps:scale web=2
2. Deploying with Docker
Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]
.dockerignore
node_modules
npm-debug.log
.env
.git
.gitignore
logs
README.md
Docker Compose (Multi-Service)
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- MONGO_URI=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
- redis
restart: always
mongo:
image: mongo:7
volumes:
- mongo_data:/data/db
restart: always
redis:
image: redis:7-alpine
restart: always
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
restart: always
volumes:
mongo_data:
Build & Run
docker-compose up -d --build
docker-compose logs -f app
docker-compose down
docker save myapp:latest | ssh user@server 'docker load'
Multi-Stage Build (Smaller Image)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
3. Deploying to a VPS (DigitalOcean, AWS EC2, Linode)
Server Setup
ssh root@your-server-ip
apt update && apt upgrade -y
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt install -y nodejs git nginx
npm install -g pm2
git clone https://github.com/your/repo.git /app
cd /app
npm ci --only=production
cat > /app/.env << EOF
NODE_ENV=production
PORT=3000
MONGO_URI=...
JWT_SECRET=...
EOF
pm2 start index.js --name "myapp"
pm2 save
pm2 startup
PM2 Configuration (ecosystem.config.js)
module.exports = {
apps: [{
name: 'myapp',
script: 'index.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
max_memory_restart: '500M',
autorestart: true,
watch: false,
max_restarts: 10,
restart_delay: 5000
}]
};
pm2 start ecosystem.config.js
pm2 monit
pm2 list
pm2 reload ecosystem.config.js
Nginx as Reverse Proxy (on VPS)
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
gzip on;
gzip_types text/plain application/json application/javascript text/css;
}
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t
systemctl restart nginx
apt install certbot python3-certbot-nginx
certbot --nginx -d yourdomain.com
Deployment Comparison
const deployments = {
heroku: {
setup: 'Minutes',
scaling: 'Easy (horizontal)',
cost: 'Pay per dyno',
control: 'Limited',
bestFor: 'Quick MVPs, startups'
},
docker: {
setup: 'Hours',
scaling: 'Via orchestrator',
cost: 'Server cost + orchestration',
control: 'Full',
bestFor: 'Microservices, consistent environments'
},
vps: {
setup: 'Half day',
scaling: 'Manual (vertical/horizontal)',
cost: 'Fixed monthly',
control: 'Complete',
bestFor: 'Full control, custom requirements'
}
};
Key Takeaways
- Heroku: Fastest setup,
git push deployment, good for MVPs
- Docker: Consistent environments across dev/prod, container orchestration
- VPS: Full control, PM2 for process management, Nginx for reverse proxy
- Always use environment variables for config
- Use PM2 with cluster mode for multi-core CPU utilization
- Set up a reverse proxy (Nginx) for SSL, load balancing, static file serving
- Enable compression, Helmet, and rate limiting in production
- Set
app.set('trust proxy', 1) when behind a reverse proxy