Deploying Node.js to the Cloud — Railway, Render & AWS

Deploying Node.js to the Cloud
An application that only runs on your laptop is a prototype. Deployment is what makes it a product. This module covers three deployment paths — from the simplest (Railway, zero config) to the most controlled (AWS EC2 with Nginx and PM2) — so you can choose the right approach for your situation.
This is Module 31 of the Node.js Full‑Stack Developer course.
Before You Deploy: Production Checklist
✅ NODE_ENV=production set
✅ All secrets in environment variables (not in code)
✅ npm start script defined in package.json
✅ Health check endpoint at GET /health
✅ Error handler returns safe messages (no stack traces in production)
✅ Logging configured (structured JSON logs)
✅ Database connection pooling configured
✅ CORS restricted to your frontend domain
✅ helmet() enabled
✅ Rate limiting enabled
✅ npm run build produces a working buildOption 1: Railway — Zero-Config PaaS
Railway is the fastest path from code to production.
Deploy from GitHub
- Go to railway.app → New Project → Deploy from GitHub
- Select your repository
- Railway auto-detects Node.js, runs
npm install, and executesnpm start
Add a Database
Dashboard → + New Service → Database → MongoDBRailway provisions a MongoDB instance and injects MONGODB_URL into your app's environment automatically.
Set Environment Variables
Dashboard → Your Service → Variables → + Add VariableAdd all variables from your .env.production. Railway injects them into process.env at runtime.
Custom Start Command
If Railway does not pick up your start command:
// package.json
{
"scripts": {
"start": "node src/server.js",
"build": "npm run build:client"
}
}Railway runs npm run build then npm start.
railway.json (Optional Config)
// railway.json
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm run build"
},
"deploy": {
"startCommand": "node src/server.js",
"healthcheckPath": "/health",
"healthcheckTimeout": 30,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}Custom Domain
Dashboard → Your Service → Settings → Custom Domain → Add domainRailway provisions a Let's Encrypt certificate automatically.
Option 2: Render — Free Tier Available
Setup
- Go to render.com → New → Web Service
- Connect GitHub repository
- Configure:
- Build Command:
npm install && npm run build - Start Command:
node src/server.js - Environment: Node
- Build Command:
render.yaml (Infrastructure as Code)
# render.yaml
services:
- type: web
name: myapp-api
runtime: node
buildCommand: npm install && npm run build
startCommand: node src/server.js
healthCheckPath: /health
envVars:
- key: NODE_ENV
value: production
- key: MONGODB_URI
fromDatabase:
name: myapp-mongo
property: connectionString
- key: JWT_ACCESS_SECRET
generateValue: true # Render generates a random value
- key: JWT_REFRESH_SECRET
generateValue: true
databases:
- name: myapp-mongo
databaseName: myapp
user: myappOption 3: AWS EC2 — Full Control
Launch an EC2 Instance
- AWS Console → EC2 → Launch Instance
- Choose Ubuntu 24.04 LTS (t3.small or larger for production)
- Create or select a key pair for SSH access
- Security Group rules:
- Allow SSH (port 22) from your IP only
- Allow HTTP (port 80) from anywhere
- Allow HTTPS (port 443) from anywhere
- Do NOT expose port 3000 publicly — Nginx proxies it
Server Setup
# SSH into the server
ssh -i your-key.pem ubuntu@your-ec2-ip
# Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js 20 via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
node -v # v20.x.x
# Install PM2 globally
npm install -g pm2
# Install Nginx
sudo apt install -y nginx
# Install Certbot (for SSL)
sudo apt install -y certbot python3-certbot-nginxDeploying the Application
# Clone repository
git clone https://github.com/yourorg/myapp.git /var/www/myapp
cd /var/www/myapp
# Install dependencies
npm ci --omit=dev
# Build the React frontend
npm run build
# Create production env file
sudo nano /var/www/myapp/.env.production
# Paste your production environment variables
# Test the app starts
node src/server.jsPM2 Process Management
# Create PM2 ecosystem config
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: 'myapp',
script: 'src/server.js',
instances: 'max', // one per CPU core
exec_mode: 'cluster', // cluster mode for load balancing
env_file: '.env.production',
error_file: '/var/log/myapp/error.log',
out_file: '/var/log/myapp/out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
max_memory_restart: '500M', // restart if memory exceeds 500MB
}]
}
EOF
# Create log directory
sudo mkdir -p /var/log/myapp
sudo chown ubuntu:ubuntu /var/log/myapp
# Start with PM2
pm2 start ecosystem.config.js
# Save PM2 process list
pm2 save
# Configure PM2 to start on system boot
pm2 startup
# Copy and run the command it outputs (sudo ...)PM2 Commands Reference
pm2 status # list all processes
pm2 logs myapp # tail logs
pm2 logs myapp --lines 100
pm2 restart myapp # restart (zero-downtime in cluster mode)
pm2 reload myapp # graceful reload (cluster mode)
pm2 stop myapp # stop process
pm2 delete myapp # remove from PM2
pm2 monit # live monitoring dashboardNginx Configuration
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect HTTP to HTTPS (Certbot adds this after SSL setup)
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
# SSL certificates (Certbot manages these)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Gzip compression
gzip on;
gzip_types text/plain application/json application/javascript text/css;
# Serve static React assets directly from Nginx (faster than Node)
location /assets/ {
root /var/www/myapp/dist;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy all other requests to Node.js
location / {
proxy_pass http://localhost: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;
proxy_read_timeout 60s;
}
}# Enable the site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # test configuration
sudo systemctl reload nginx
# Obtain SSL certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comCI/CD with GitHub Actions
Automate deployment on every push to main:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# For Railway — trigger deploy via Railway API
- name: Deploy to Railway
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.RAILWAY_TOKEN }}" \
https://backboard.railway.app/graphql/v2 \
-d '{"query":"mutation { deploymentTrigger(input: { environmentId: \"${{ secrets.RAILWAY_ENV_ID }}\" }) { id } }"}'
# For EC2 — SSH and pull latest code
- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --omit=dev
npm run build
pm2 reload myappMonitoring
# PM2 built-in monitoring
pm2 monit
# Install pm2-logrotate to prevent log files from growing indefinitely
pm2 install pm2-logrotate
# Health check via curl
curl https://yourdomain.com/health
# {"status":"ok","uptime":12345,"timestamp":"2026-05-12T..."}For production monitoring, set up an external uptime monitor (Better Uptime, UptimeRobot) that alerts you when the health check fails.
Node.js Full‑Stack Course — Module 31 of 32
Your application is live in production. Continue to Module 32 to learn performance optimisation with clustering and PM2.
Summary
Three deployment options cover the full spectrum of Node.js production needs:
- Railway: connect GitHub → set env vars → deploy. Managed databases, custom domains, auto-SSL. Best for fast shipping
- Render: similar to Railway with a free tier.
render.yamlenables infrastructure-as-code - AWS EC2: Ubuntu + NVM + PM2 + Nginx. Full control over server configuration. PM2 cluster mode for multi-core utilisation; Nginx for SSL, static files, and reverse proxying
pm2 startup+pm2 savekeeps your Node.js process alive across server reboots- GitHub Actions CI/CD runs tests on every PR and deploys to production on merge to
main - Never expose Node.js port (3000) publicly — always proxy via Nginx or the platform's routing layer
- Set up an external health check monitor so you are alerted before users are
Continue to Module 32: Performance, Clustering & PM2 →
