How to Deploy a Next.js App on AWS EC2 (Nginx + Ubuntu)

Vercel makes deploying Next.js almost trivial, and for a lot of teams that's the right call. But if you want full control over the box. Your own networking, your own pricing, everything living inside one AWS account. Deploying to a plain EC2 instance is worth knowing. This is the full path: Node, a GitHub deploy key, a production build kept alive by PM2, Nginx as a reverse proxy, and a free SSL certificate at the end so traffic is encrypted.
A quick reminder on why Next.js earns the effort: it gives you Server-Side Rendering and Static Site Generation out of the box, so you get server-generated HTML, faster loads, and an SEO-friendly architecture without bolting it on yourself.
This guide assumes you've already opened a secured SSH connection to your EC2 instance. If you haven't set that up yet, our PuTTY SSH guide covers it. Once you're connected, follow the steps below.
Step 1: Your starting point
This is roughly the screen you should be looking at before you begin. A fresh shell on the instance.

Step 2: Update packages and add the NodeSource key
First, refresh the system package index:
sudo apt-get update
Then download and import the NodeSource GPG key so apt trusts the Node repository:
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpgStep 3: Add the Node repository and install Node.js
Create a deb repository pinned to the Node version you want, set with NODE_MAJOR=#version. Pick an active LTS line: at the time of writing that is 22 (supported to April 2027) or 24 (to April 2028). Node 16, 18, 20 and 21 have all reached end of life and no longer receive security patches, so avoid them on a server you intend to leave running. We'll use 22 here.
NODE_MAJOR=22 # install an active LTS line
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.listUpdate apt again so it picks up the new repository, then install Node:
sudo apt-get update # refresh package lists
sudo apt-get install nodejs -y # install the Node version set above
If you ever need to remove Node.js entirely, this undoes all of the above:
sudo apt-get purge nodejs && \
rm -r /etc/apt/sources.list.d/nodesource.list && \
rm -r /etc/apt/keyrings/nodesource.gpgConfirm the version you just installed:

Step 4: Create an SSH key pair
You'll need an SSH key pair so the instance can talk to your GitHub repository securely. On UNIX and UNIX-like systems, ssh-keygen does this. The -t flag picks the key type and -b sets the bit length. Here we generate a 2048-bit RSA key:
ssh-keygen -b 2048 -t rsa # create your SSH key
Step 5: Copy the public key
You start in your home directory by default, so you only need to step into .ssh to read the public key:
cd .ssh # enter the .ssh directory
ls # list the files inside
cat id_rsa.pub # print the public key, then copy its contents
Step 6: Add the key as a GitHub deploy key
Open your GitHub repository and go to its Settings.

In the left-hand menu, scroll down to Deploy keys and click Add deploy key.

Give the key a name and paste the value you copied into the Key field. Leave Allow write access unchecked. The instance only needs to read from the repo, never push to it.

Step 7: Copy the repository's SSH clone command
Back on the repository page, grab the SSH clone URL for the repo.

Step 8: Clone the repository on the instance
Back in your terminal session, clone the repo using the SSH URL you just copied:
git clone "your GitHub SSH clone URL"
Step 9: Install dependencies
Move into the project directory and install everything:
ls # list the directories you just cloned
cd "your directory name" # move into the app's root
npm i # install all dependencies
Step 10: Build for production
Generate an optimized production build, and install sharp so Next.js can handle image optimization automatically:
npm run build
npm install sharp # lets Next.js optimize images automatically
Step 11: Install PM2
You need something to keep the Next.js process alive in the background after you close the terminal. PM2 is the standard tool for that. It runs, restarts, and supervises the process for you. Install it globally:
sudo npm install pm2 -gStep 12: Run Next.js under PM2
Start the app through PM2 so it survives terminal disconnects and can be stopped or restarted at will:
pm2 start npm --name nextjs-app -- run start -- -p 3000Check the status of the process at any time:
pm2 list nextjs-app
Step 13: Set up the firewall
Enable UFW and open the ports you actually need. SSH, HTTP, and HTTPS:
sudo ufw enable # enable the firewall
sudo ufw status # check the firewall status
sudo ufw allow ssh # port 22
sudo ufw allow http # port 80
sudo ufw allow https # port 443
Step 14: Install Nginx
sudo apt install nginx
Step 15: Configure Nginx as a reverse proxy
Nginx's job here is to sit in front of your app as a reverse proxy with caching. Open its default site config:
sudo nano /etc/nginx/sites-available/defaultFind the server_name line and the location / block, and replace them with the following (swap in your own domain):
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:5000; # whatever port your app runs on
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;
}
Make sure theproxy_passport matches the port your app actually listens on. If you started PM2 with-p 3000, proxy tohttp://localhost:3000.
Test the config and restart Nginx to load it:
sudo nginx -t # test the Nginx configuration
sudo service nginx restart # restart Nginx to apply the changes
Step 16: Point your domain at the instance
Copy the public IP address of your EC2 instance.

In your DNS provider (Route 53 here), paste it into the main Type A record. If one doesn't exist yet, create the record.

Visit your domain now and you'll see it's served over plain HTTP (port 80) and flagged as not secure.

Step 17: Add a free SSL certificate with Certbot
Install Certbot with its Nginx plugin, then request Let's Encrypt certificates for your domain. Certbot updates the Nginx config and reloads it for you:
sudo apt install certbot python3-certbot-nginx
sudo apt-get update # update packages
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # request SSL/TLS certificates
Refresh the browser and your traffic is now encrypted over HTTPS.

That's the whole pipeline: a production Next.js build kept alive by PM2, fronted by Nginx, with a firewall and an auto-renewing SSL certificate. It's more moving parts than a one-click deploy, but it gives you full control over the box. If you'd rather hand this off, tell us about your project. Or see what we've shipped.
Thinking about building this?
Appluex designs and ships production mobile & web apps. Including AI features. Let's talk.