Node.jsBackendFull-Stack

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

TT
TopicTrick Team
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

text
✅ 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 build

Option 1: Railway — Zero-Config PaaS

Railway is the fastest path from code to production.

Deploy from GitHub

  1. Go to railway.app → New Project → Deploy from GitHub
  2. Select your repository
  3. Railway auto-detects Node.js, runs npm install, and executes npm start

Add a Database

text
Dashboard → + New Service → Database → MongoDB

Railway provisions a MongoDB instance and injects MONGODB_URL into your app's environment automatically.

Set Environment Variables

text
Dashboard → Your Service → Variables → + Add Variable

Add 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:

json
// 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)

json
// 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

text
Dashboard → Your Service → Settings → Custom Domain → Add domain

Railway provisions a Let's Encrypt certificate automatically.


Option 2: Render — Free Tier Available

Setup

  1. Go to render.com → New → Web Service
  2. Connect GitHub repository
  3. Configure:
    • Build Command: npm install && npm run build
    • Start Command: node src/server.js
    • Environment: Node

render.yaml (Infrastructure as Code)

yaml
# 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: myapp

Option 3: AWS EC2 — Full Control

Launch an EC2 Instance

  1. AWS Console → EC2 → Launch Instance
  2. Choose Ubuntu 24.04 LTS (t3.small or larger for production)
  3. Create or select a key pair for SSH access
  4. 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

bash
# 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-nginx

Deploying the Application

bash
# 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.js

PM2 Process Management

bash
# 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

bash
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 dashboard

Nginx Configuration

nginx
# /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;
    }
}
bash
# 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.com

CI/CD with GitHub Actions

Automate deployment on every push to main:

yaml
# .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 myapp

Monitoring

bash
# 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.yaml enables 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 save keeps 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 →