MervCodes

Tech Reviews From A Programmer

How to Deploy a Node.js App to AWS EC2 (Step-by-Step Guide)

8 min read

Deploying a Node.js app to AWS EC2 is one of those tasks that looks intimidating until you've done it once. Then it becomes second nature. The issue is that most guides either oversimplify it (missing production essentials like process management and reverse proxies) or bury you in AWS console screenshots that change every quarter. This guide cuts through the noise and gets you running with a properly configured, production-grade setup in about 30 minutes.

TL;DR: Deploy Node.js apps to AWS EC2 with this production-ready guide. Learn instance setup, PM2, Nginx, SSL, and automated deployments.

Why EC2 for Node.js?

Before we dive in, let's be clear about the trade-offs. EC2 gives you full control and flexibility—perfect for complex applications with specific requirements. It's cheaper than managed platforms like Heroku or Railway when you're scaling beyond a hobby project. The downside? You're responsible for updates, security patches, and monitoring. For most mid-sized applications, this is the right call. If you want fully managed hosting with zero ops work, consider Vercel or Railway instead.

Prerequisites

You'll need:

  • An AWS account (free tier works, but you'll likely use paid resources)
  • A Node.js application ready to deploy (git repo preferred)
  • SSH client on your machine (built-in on Mac/Linux, use Git Bash or WSL on Windows)
  • Basic familiarity with the Linux command line

Step 1: Launch an EC2 Instance

Log into your AWS Console and navigate to EC2. Click "Launch Instances."

Configuration settings:

  • AMI: Select "Ubuntu Server 24.04 LTS" (stable, well-documented for Node.js)
  • Instance type: t3.micro for testing, t3.small or t3.medium for production depending on traffic
  • Key pair: Create a new one if you don't have one. Download the .pem file and store it securely. Never commit this to git.
  • Network settings: Create a new security group or use existing. You'll configure it in the next step.
  • Storage: 20 GB is fine to start; adjust based on your needs.

Click "Launch instance" and wait 30 seconds for it to initialize.

Step 2: Configure Security Groups

In the EC2 dashboard, find your instance and click on its security group. Edit inbound rules to allow:

Protocol Port Source
SSH 22 Your IP (or 0.0.0.0/0 for testing only)
HTTP 80 0.0.0.0/0
HTTPS 443 0.0.0.0/0

Save the rules. In production, restrict SSH to your specific IP. If you don't know your IP, run:

curl ifconfig.me

Step 3: SSH Into Your Instance

Copy your instance's public IPv4 address from the EC2 dashboard. Then:

chmod 600 /path/to/your/key.pem
ssh -i /path/to/your/key.pem ubuntu@your-instance-ip

You're now inside your EC2 instance. Immediately update the system:

sudo apt update && sudo apt upgrade -y

Step 4: Install Node.js and npm

Using NodeSource's repository ensures you get the latest LTS version:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

Verify installation:

node --version
npm --version

You should see Node.js 22.x and npm 10.x or later.

Step 5: Clone Your Application

Create a deployment directory and clone your repo:

mkdir -p /home/ubuntu/apps
cd /home/ubuntu/apps
git clone https://github.com/yourusername/your-app.git
cd your-app

Install dependencies:

npm install --production

The --production flag skips dev dependencies, reducing install time and instance size.

Step 6: Set Up Environment Variables

Create a .env file in your app directory:

sudo nano /home/ubuntu/apps/your-app/.env

Add your environment variables:

NODE_ENV=production
PORT=3000
DATABASE_URL=your-database-url
API_KEY=your-api-key

Save with Ctrl+X, then Y, then Enter. Make sure this file is in your .gitignore so you never commit secrets.

Step 7: Install and Configure PM2

PM2 is a production process manager that keeps your Node.js app running and automatically restarts it if it crashes.

sudo npm install -g pm2

Start your application with PM2:

cd /home/ubuntu/apps/your-app
pm2 start npm --name "my-app" -- start

If you run npm start in your package.json, this works out of the box. If your entry file is different:

pm2 start server.js --name "my-app"

Configure PM2 to start on system reboot:

pm2 startup
pm2 save

Follow the instructions output by pm2 startup—it'll give you a sudo command to run. This ensures your app survives instance restarts.

Verify PM2 is managing your app:

pm2 list
pm2 logs my-app

Step 8: Install and Configure Nginx

Nginx acts as a reverse proxy, routing traffic from port 80 (HTTP) to your Node.js app on port 3000. It also handles SSL termination and static file serving efficiently.

sudo apt install -y nginx

Create an Nginx configuration file:

sudo nano /etc/nginx/sites-available/default

Replace the entire file with this configuration:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name your-domain.com www.your-domain.com;

    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_cache_bypass $http_upgrade;
        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;
    }
}

Test the configuration:

sudo nginx -t

You should see "syntax is ok" and "test is successful". Reload Nginx:

sudo systemctl restart nginx

Now visit http://your-instance-ip in your browser. You should see your Node.js app running.

Step 9: Set Up HTTPS with Let's Encrypt

Never run a production app without HTTPS. Use Certbot with Let's Encrypt for free SSL certificates:

sudo apt install -y certbot python3-certbot-nginx

Obtain a certificate. Replace your-domain.com with your actual domain (and make sure you've pointed DNS to your instance):

sudo certbot certonly --nginx -d your-domain.com -d www.your-domain.com

Now update your Nginx configuration to use SSL:

sudo nano /etc/nginx/sites-available/default

Replace it with:

server {
    listen 80;
    listen [::]:80;
    server_name your-domain.com www.your-domain.com;

    location / {
        return 301 https://$server_name$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your-domain.com www.your-domain.com;

    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    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_cache_bypass $http_upgrade;
        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;
    }
}

Test and reload:

sudo nginx -t
sudo systemctl restart nginx

Certbot automatically renews your certificate 30 days before expiration. Verify renewal will work:

sudo certbot renew --dry-run

Step 10: Set Up Automated Deployments (Optional but Recommended)

Deploying updates manually via SSH is painful. Use a simple git hook workflow instead.

On your EC2 instance, create a bare git repository:

mkdir -p /home/ubuntu/repos/your-app.git
cd /home/ubuntu/repos/your-app.git
git init --bare

Create a post-receive hook:

nano hooks/post-receive

Add this script:

#!/bin/bash
WORK_TREE=/home/ubuntu/apps/your-app

git --work-tree=$WORK_TREE --git-dir=$(pwd) checkout -f
cd $WORK_TREE
npm install --production
pm2 restart my-app

Make it executable:

chmod +x hooks/post-receive

On your local machine, add this remote to your git config:

git remote add production ssh://ubuntu@your-instance-ip/home/ubuntu/repos/your-app.git

Now deployments are as simple as:

git push production main

Your app will automatically pull the latest code, reinstall dependencies, and restart.

Monitoring and Maintenance

Check your app status regularly:

pm2 list
pm2 monit

View logs in real-time:

pm2 logs my-app

Monitor system resources:

top
df -h

Common Pitfalls

App crashes on restart: Ensure your .env file has all required variables and the file permissions allow PM2 to read it.

"Cannot GET /" error: Check that your app is listening on the correct port (3000 by default). Verify with curl http://localhost:3000.

502 Bad Gateway: Nginx can't reach your Node.js app. Check that PM2 is running and Nginx configuration is correct.

Port already in use: If port 3000 is already in use, kill it or change the port in your app and Nginx config.

What's Next?

Once you've got the basics running, consider:

  • Setting up CloudWatch monitoring and alarms for CPU/memory
  • Using RDS for managed databases instead of self-hosting
  • Implementing automated backups and disaster recovery
  • Using AWS CodePipeline for CI/CD instead of git hooks
  • Scaling horizontally with load balancing across multiple instances

This setup handles most production workloads efficiently. You've got process management, a reverse proxy, SSL, and automated deployments—the core essentials for a reliable Node.js deployment.

Sources

  1. Node.js Documentation
  2. MDN Web Docs
  3. npm Documentation

Looking for more? Check out Adaptels.

Related Articles

How to Debug Node.js Memory Leaks (Step-by-Step Guide)

Learn how to detect, diagnose, and fix Node.js memory leaks using heap snapshots, Chrome DevTools, and clinic.js — with real code examples.

How to Set Up GitHub Actions for CI/CD (Beginner-Friendly Guide)

Learn how to set up GitHub Actions for CI/CD pipelines — from your first workflow file to automated deployments with real YAML examples.

Running Local LLMs With Ollama: Developer Setup Guide

Set up Ollama to run local LLMs on your machine. Covers installation, model selection, API usage, and integrating local models into your dev workflow.