Skip to main content

Deploy Next.js on AWS EC2 — complete walkthrough

TL;DR

Step-by-step guide to deploying a self-hosted Next.js application on AWS EC2 with standalone output, PM2, nginx, and CloudWatch.

Key facts

Topology
Next.js on AWS EC2

TL;DR

This guide walks through deploying a self-hosted Next.js application on AWS EC2 from instance selection through PM2, nginx, CloudWatch integration, and deployment automation — a production-grade setup that gives you full control over your Next.js infrastructure.

Step 1 — EC2 instance selection

For a typical Next.js application:

  • Small/medium traffic: t3.medium (2 vCPU, 4 GB) — burstable, cost-effective
  • Sustained traffic: c6g.large (2 vCPU ARM, 4 GB) — better price/performance on Graviton
  • Heavy SSR workloads: c6g.xlarge (4 vCPU ARM, 8 GB) — dedicated compute for server-side rendering

Use an Ubuntu 24.04 AMI. EBS volume: 30 GB gp3 minimum (gp3 is cheaper and faster than gp2).

Step 2 — Security groups

Create a security group with these inbound rules:

PortSourcePurpose
22Your IP / VPN CIDRSSH access
800.0.0.0/0HTTP (redirects to HTTPS)
4430.0.0.0/0HTTPS
aws ec2 create-security-group --group-name nextjs-sg --description "Next.js production"
aws ec2 authorize-security-group-ingress --group-name nextjs-sg \
    --protocol tcp --port 22 --cidr YOUR_IP/32
aws ec2 authorize-security-group-ingress --group-name nextjs-sg \
    --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-name nextjs-sg \
    --protocol tcp --port 443 --cidr 0.0.0.0/0

Step 3 — Server setup and Node.js

SSH into the instance and prepare the environment:

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git build-essential nginx

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2

Step 4 — Deploy with standalone output mode

sudo mkdir -p /var/www/nextapp
sudo chown ubuntu:ubuntu /var/www/nextapp
cd /var/www/nextapp
git clone git@github.com:yourorg/nextapp.git .
npm ci

Ensure your next.config.js has output: 'standalone', then build:

npm run build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static

Step 5 — PM2 ecosystem configuration

Create ecosystem.config.js:

module.exports = {
  apps: [{
    name: 'nextapp',
    script: '.next/standalone/server.js',
    instances: 'max',
    exec_mode: 'cluster',
    max_memory_restart: '1G',
    node_args: '--max-old-space-size=1536',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      HOSTNAME: '0.0.0.0',
    },
    wait_ready: true,
    listen_timeout: 10000,
    kill_timeout: 5000,
    error_file: '/var/log/pm2/nextapp-error.log',
    out_file: '/var/log/pm2/nextapp-out.log',
    merge_logs: true,
  }],
};
sudo mkdir -p /var/log/pm2
sudo chown ubuntu:ubuntu /var/log/pm2
pm2 start ecosystem.config.js
pm2 startup systemd
pm2 save

Step 6 — nginx with proper proxy headers

Create /etc/nginx/sites-available/nextapp:

upstream nextjs_backend {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name example.com;

    gzip on;
    gzip_types text/plain application/json application/javascript text/css image/svg+xml;

    location /_next/static {
        alias /var/www/nextapp/.next/static;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://nextjs_backend;
        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_set_header X-Forwarded-Host $host;
        proxy_read_timeout 120s;
        proxy_connect_timeout 10s;
    }
}
sudo ln -s /etc/nginx/sites-available/nextapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Step 7 — SSL and firewall

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable

Step 8 — CloudWatch integration

Install the CloudWatch agent for system-level metrics:

wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb

Create a config at /opt/aws/amazon-cloudwatch-agent/etc/config.json to track memory, disk, and PM2 log files. Start the agent:

sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
    -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json

Set up CloudWatch alarms for CPU > 80%, memory > 85%, and disk > 80%.

ALB considerations

For multi-instance deployments, place an Application Load Balancer in front:

  • Target group health check: /api/health on port 3000
  • Sticky sessions: Enable if your app uses server-side sessions
  • SSL termination: Let the ALB handle SSL and send HTTP to instances (cheaper, simpler)

Deployment script

#!/bin/bash
set -euo pipefail

APP_DIR="/var/www/nextapp"
cd "$APP_DIR"

git fetch origin main
git reset --hard origin/main
npm ci
npm run build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static

pm2 reload ecosystem.config.js

sleep 5
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/api/health)
if [ "$HTTP_CODE" != "200" ]; then
  echo "Health check failed — investigate immediately"
  exit 1
fi

echo "Deployment successful"

With Reflex

Reflex provides unified monitoring and deployment orchestration for Next.js on EC2. It tracks PM2 process health, nginx upstream errors, memory trends, and disk usage across your instances. During deployments, Reflex orchestrates rolling updates with health-gated rollouts, and can automatically roll back if the health check fails — replacing manual deployment scripts with audited, repeatable automation. See How it works.