Follow Us

CodeWithSabir

  • Contact Us
  • Privacy Policy
  • About
  • Terms & Conditions

All Rights Reserved © 2026

  • Light
  • Dark
DevOps

Deploying Next.js Apps on AWS EC2: A Step-by-Step Guide

Sabir Soft
Sabir Lkhaloufi
  • March 15, 2026
  • 4 min read

Deploying Next.js Apps on AWS EC2: A Step-by-Step Guide

Vercel is the easiest way to deploy Next.js, but not every project can use it — budget constraints, compliance requirements, need for custom server configuration, or just wanting full control. AWS EC2 gives you a real Linux server you can configure exactly how you need.

This guide takes you from a fresh EC2 instance to a production-ready Next.js deployment with HTTPS, process management, and automatic restarts.

Step 1: Launch and Configure the EC2 Instance

In the AWS Console:

  • AMI: Ubuntu 24.04 LTS
  • Instance type: t3.small for most apps (t3.micro if you're on free tier)
  • Security group: Open ports 22 (SSH), 80 (HTTP), 443 (HTTPS)
  • Key pair: Create or select one — you'll need the .pem file

After launch, get your instance's public IP from the console.

Step 2: Connect and Install Dependencies

# Connect via SSH
chmod 400 your-key.pem
ssh -i your-key.pem ubuntu@YOUR_EC2_IP
 
# Update system
sudo apt update && sudo apt upgrade -y
 
# Install Node.js 20 via NodeSource
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
 
# Install PM2 globally
sudo npm install -g pm2
 
# Install Nginx
sudo apt install -y nginx
 
# Verify
node --version  # v20.x.x
pm2 --version
nginx -v

Step 3: Set Up Your Application

# Create app directory
sudo mkdir -p /var/www/myapp
sudo chown ubuntu:ubuntu /var/www/myapp
 
# Clone your repository
cd /var/www/myapp
git clone https://github.com/yourusername/your-nextjs-app.git .
 
# Install dependencies
npm ci --production=false
 
# Build the Next.js app
npm run build

Create your environment file:

# /var/www/myapp/.env.production
nano .env.production
NODE_ENV=production
DATABASE_URL=your_database_url
NEXTAUTH_SECRET=your_secret
NEXTAUTH_URL=https://yourdomain.com

Step 4: Configure PM2

PM2 keeps your Node.js process alive, restarts on crash, and starts on server reboot:

// /var/www/myapp/ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'nextjs-app',
      script: 'node_modules/.bin/next',
      args: 'start',
      cwd: '/var/www/myapp',
      instances: 'max',        // Use all CPU cores
      exec_mode: 'cluster',    // Cluster mode for multi-core
      watch: false,
      max_memory_restart: '1G',
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      error_file: '/var/log/pm2/nextjs-error.log',
      out_file: '/var/log/pm2/nextjs-out.log',
      merge_logs: true,
      time: true,
    },
  ],
}
# Create log directory
sudo mkdir -p /var/log/pm2
sudo chown ubuntu:ubuntu /var/log/pm2
 
# Start the app
pm2 start ecosystem.config.js --env production
 
# Save PM2 config so it restarts after server reboot
pm2 save
 
# Set up PM2 to start on boot
pm2 startup
# Run the command it outputs (usually: sudo env PATH=... pm2 startup ...)

Step 5: Configure Nginx as Reverse Proxy

Nginx sits in front of your Node.js app — handling SSL, gzip compression, and static assets:

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
 
    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;
 
    # SSL — will be filled in by Certbot
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
 
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 
    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 1000;
 
    # Next.js static assets — cache aggressively
    location /_next/static/ {
        alias /var/www/myapp/.next/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
 
    # Public folder
    location /public/ {
        alias /var/www/myapp/public/;
        expires 1d;
    }
 
    # Proxy everything else to Next.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 rm /etc/nginx/sites-enabled/default
 
# Test config
sudo nginx -t
 
# Reload
sudo systemctl reload nginx

Step 6: SSL with Let's Encrypt

# Install Certbot
sudo apt install -y certbot python3-certbot-nginx
 
# Get certificate (temporarily opens port 80)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
 
# Certbot automatically modifies your Nginx config and sets up auto-renewal
# Verify renewal will work
sudo certbot renew --dry-run

Your site is now live at https://yourdomain.com.

Step 7: Automated Deployments

Create a deployment script:

# /var/www/myapp/deploy.sh
#!/bin/bash
set -e
 
echo "Starting deployment..."
cd /var/www/myapp
 
# Pull latest code
git pull origin main
 
# Install new dependencies
npm ci --production=false
 
# Build
npm run build
 
# Reload PM2 with zero downtime
pm2 reload ecosystem.config.js --env production
 
echo "Deployment complete!"
chmod +x deploy.sh

For GitHub Actions automation, add a deploy job that SSH's in and runs this script — exactly like the CI/CD pipeline covered in the GitHub Actions article on this blog.

Monitoring and Logs

# Real-time logs
pm2 logs nextjs-app --lines 100
 
# Process status
pm2 status
 
# CPU/memory usage
pm2 monit
 
# Nginx access logs
sudo tail -f /var/log/nginx/access.log
 
# Nginx error logs
sudo tail -f /var/log/nginx/error.log

Common Mistakes

1. Not setting up a firewall. Enable UFW:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

2. Running Node as root. Use the ubuntu user, not root.

3. Forgetting NEXTAUTH_URL in production. NextAuth needs to know the production URL — without it, OAuth callbacks fail.

4. Not configuring swap. Small instances can run out of RAM during npm run build. Add 2GB swap:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

5. Not caching static assets in Nginx. Next.js generates hashed filenames for JS/CSS — they can be cached permanently. The Nginx config above handles this.

Key Takeaways

  • PM2 cluster mode uses all CPU cores and handles zero-downtime reloads
  • Nginx handles SSL, gzip, and static asset caching — keep Node.js only for dynamic requests
  • Let's Encrypt SSL is free and auto-renews via Certbot
  • Add swap space on small instances to prevent OOM during builds
  • Always set up UFW firewall — close all ports except 22, 80, 443
  • Keep your deploy script simple: pull, install, build, reload
Popular Blogs
Claude AI vs ChatGPT: An Honest Comparison for Developers
  • April 28, 2026
AI Tools Every Developer Should Be Using in 2026
  • April 20, 2026
Using the Claude API in Real Projects: A Practical Developer Guide
  • April 15, 2026
Prompt Engineering for Developers: Write Prompts That Actually Work
  • April 10, 2026
Categories
AIDevOpsNext.jsMobile DevelopmentWeb Development

Related Posts

DevOps
CI/CD Pipeline with GitHub Actions and Docker: A Complete Guide
Sabir Khaloufi·Mar 25, 2026
DevOps
Docker for Developers: From Zero to Production-Ready
Sabir Khaloufi·Mar 20, 2026
DevOps
Kubernetes Basics Every Developer Should Know
Sabir Khaloufi·Mar 10, 2026