ShortIQ

ShortIQ

Deployment

How to Deploy a Node.js App on AWS EC2 with Nginx and PM2

A step-by-step guide to deploying a Node.js application on AWS EC2 with Nginx as a reverse proxy, PM2 for process management, SSL with Let Encrypt Certbot, and environment variable management for production.

June 12, 2026ShortIQ Editorial Team

Prerequisites and EC2 Instance Setup

This guide deploys a Node.js application (Express.js, Fastify, or any HTTP server) on an AWS EC2 Ubuntu 24.04 instance with Nginx as the reverse proxy, PM2 for process management, and SSL certificates from the ACME certificate authority via Certbot. You need an AWS account, a domain name with DNS you can configure, and your Node.js application code in a git repository.

Launch an EC2 instance in the AWS console. Choose Ubuntu 24.04 LTS as the AMI. Choose t3.small (2 vCPU, 2GB RAM) as a minimum for a production application. Create a new key pair and download the .pem file to your local machine. Configure the Security Group to allow SSH (port 22) from your IP only, HTTP (port 80) from anywhere, and HTTPS (port 443) from anywhere. Do not expose the Node.js port (typically 3000 or 5000) directly — Nginx will proxy requests to it.

bash
# Connect to your EC2 instance
chmod 400 your-key.pem
ssh -i your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP

Install Node.js with NVM

Install NVM (Node Version Manager) so you can install and switch Node.js versions without sudo. NVM installs Node.js in your home directory which is more secure than system-wide installation and allows different Node versions per project.

bash
# Update system packages
sudo apt update && sudo apt upgrade -y

# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Reload shell
source ~/.bashrc

# Install Node.js LTS
nvm install --lts
nvm use --lts
nvm alias default node

# Verify installation
node --version
npm --version

Clone and Configure Your Application

Clone your application from git. If the repository is private, configure an SSH deploy key or use a personal access token. Install dependencies and set up environment variables.

bash
# Clone your application
git clone https://github.com/youruser/yourapp.git
cd yourapp

# Install dependencies (production only)
npm install --omit=dev

# Create the environment file
nano .env
# Add your production environment variables:
# NODE_ENV=production
# PORT=5000
# DATABASE_URL=...
# JWT_SECRET=...

# Test that the app starts
node dist/index.js

Configure PM2 for Process Management

PM2 keeps your Node.js application running, restarts it on crashes, starts it automatically on server reboot, and provides process monitoring. Install PM2 globally and configure it to start on boot.

bash
# Install PM2 globally
npm install -g pm2

# Start your application with PM2
pm2 start dist/index.js --name myapp

# Or use an ecosystem config file for more control
# Create ecosystem.config.js:
# module.exports = {
#   apps: [{ name: "myapp", script: "dist/index.js",
#     env_production: { NODE_ENV: "production", PORT: 5000 } }]
# };
pm2 start ecosystem.config.js --env production

# Save PM2 process list
pm2 save

# Generate systemd startup script
pm2 startup
# Run the command that PM2 prints

# Verify it is running
pm2 status
pm2 logs myapp

Install and Configure Nginx

Nginx acts as a reverse proxy sitting in front of your Node.js application. It handles SSL termination, serves static files efficiently, adds security headers, and forwards HTTP requests to the Node.js port.

bash
# Install Nginx
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx

# Create a new Nginx site configuration
sudo nano /etc/nginx/sites-available/myapp

# Paste this configuration (replace yourdomain.com and port 5000):
# server {
#   listen 80;
#   server_name yourdomain.com www.yourdomain.com;
#
#   location / {
#     proxy_pass http://localhost:5000;
#     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;
#   }
# }

# Enable the site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

# Test the configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Point Your Domain and Enable SSL

In your DNS provider, create an A record pointing your domain to the EC2 public IP address. Wait for DNS propagation (usually 5-15 minutes, sometimes up to an hour). Then install Certbot to obtain a free SSL certificate from the ACME certificate authority.

bash
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y

# Obtain SSL certificate (replace yourdomain.com)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Follow the prompts:
# - Enter email for renewal notices
# - Agree to terms of service
# - Choose to redirect HTTP to HTTPS (recommended: option 2)

# Certbot automatically updates the Nginx config
# and sets up auto-renewal via a systemd timer

# Verify auto-renewal is working
sudo certbot renew --dry-run

Deployment Update Workflow

For subsequent deployments, SSH into the server, pull the latest code, rebuild if needed, and reload PM2 with zero downtime using the reload command (which uses a graceful restart rather than killing and restarting the process).

bash
# SSH into the server
ssh -i your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP

# Pull latest code
cd yourapp
git pull origin main

# Install any new dependencies
npm install --omit=dev

# Rebuild TypeScript if applicable
npm run build

# Reload PM2 (zero-downtime restart)
pm2 reload myapp

# Verify the deployment
pm2 status
curl http://localhost:5000/health

Security Hardening

After the basic deployment, apply security hardening steps: restrict SSH access, configure the firewall with UFW, set up automatic security updates, and add security headers to Nginx.

  • Enable UFW firewall: sudo ufw allow OpenSSH && sudo ufw allow Nginx Full && sudo ufw enable
  • Disable SSH password login and use key-based authentication only: PasswordAuthentication no in /etc/ssh/sshd_config
  • Enable automatic security updates: sudo dpkg-reconfigure --priority=low unattended-upgrades
  • Add Nginx security headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy
  • Monitor disk space and memory usage: set up CloudWatch alarms on the EC2 instance for CPU, memory, and disk
  • Rotate your .env secrets regularly and never commit them to git

FAQ

Should I use AWS EC2 or a managed service like Elastic Beanstalk or App Runner?

EC2 gives you full control and is cheapest at scale, but requires you to manage the OS, Nginx, PM2, security updates, and SSL yourself. Elastic Beanstalk handles scaling and deployment automation but adds complexity. AWS App Runner is the simplest managed Node.js deployment: connect a container registry or GitHub repo and App Runner handles everything. For small teams or solo developers who want minimal ops overhead, App Runner or Railway are better starting points than EC2. Use EC2 when you need specific configuration, want to colocate multiple services on one server, or need to optimise costs at scale.

What instance type should I use for a Node.js production app?

t3.small (2 vCPU, 2GB RAM) is a reasonable starting point for a small production app. t3.medium (2 vCPU, 4GB RAM) is better for apps with moderate traffic. The t3 family uses burstable performance which is cost-effective for web servers with variable traffic. Monitor your actual CPU and memory usage in CloudWatch for the first month and resize up or down accordingly. Always add a swap file (1-2GB) as a safety net against out-of-memory crashes.

How do I automate deployments to EC2?

Use GitHub Actions with a deploy step that SSHes into the server and runs the git pull, npm install, npm run build, and pm2 reload commands. Store the SSH private key as a GitHub Actions secret. An alternative is AWS CodeDeploy, which pulls a deployment package from S3 or GitHub and runs lifecycle hook scripts on the EC2 instance. For a fully automated pipeline, consider moving to a container-based approach with ECR and ECS, which integrates more cleanly with AWS deployment pipelines.

What is the difference between pm2 restart and pm2 reload?

pm2 restart kills the process and starts a fresh one — there is a brief period when no process is handling requests. pm2 reload performs a graceful restart: it starts the new process, waits for it to be ready, moves traffic to it, then kills the old process. For zero-downtime deployments, always use pm2 reload. Reload works correctly when your application handles the SIGINT signal gracefully (stops accepting new connections and waits for in-flight requests to complete before exiting).

Related free tools

If you want to turn this topic into action, use one of ShortIQ's free tools for campaign planning, UTM structure, or QR distribution.

Continue Reading

Explore more guides on link shortener SaaS strategy, Bitly alternatives, and white label link management.

Free newsletter

Get new guides in your inbox

We publish practical guides on dev tooling, prompt engineering, marketing workflows, and deployment. No fluff — straight to the point.

No spam. Unsubscribe any time.

Was this article helpful?

Tell us if this guide solved the problem or what was still missing. We use this to improve the blog and only follow up if you explicitly allow it.

We use this to improve tutorials, examples, and technical depth.