Deploying a Node.js API to AWS
I walk through launching an EC2 instance, setting up PM2 as a process manager, and configuring nginx as a reverse proxy for a Node.js API.

Running node index.js locally and testing your API with Postman is satisfying, but it’s not deployment. The process disappears the moment you close the terminal. Nobody outside your machine can reach it. Step away, and the server is gone.
Getting that API onto a server you can hand a URL to requires four things working together: a virtual machine to run on, a process manager to keep Node.js alive, a reverse proxy to handle HTTP traffic on port 80, and a static IP address so the address doesn’t shift every time the instance restarts.
Quick answer: Launch an EC2 instance (Amazon Linux 2023,
t2.micro), open ports 22, 80, and 443 in the security group, SSH in, install Node.js with NVM, start your API with PM2, configure nginx to forward port 80 to your Node.js port, then assign an Elastic IP so the address is permanent.
Series: Part 3 of 4. Start with Part 1: Building a REST API with Node.js and Express · Previous: Structuring a Production-Ready Express Project
On this page
- Launch your EC2 instance and open the right ports
- SSH in and install Node.js
- Keep the API running with PM2
- Put nginx in front of your API
- Assign an Elastic IP so the address sticks
- What breaks and how to spot it
- The API that survives a reboot
Launch your EC2 instance and open the right ports
In the EC2 console, choose “Launch instance.” For the AMI, select Amazon Linux 2023 — it’s the current default and receives regular security updates from AWS. For instance type, t2.micro is free-tier eligible and handles most small APIs without issue.
When you reach “Key pair (login),” create a new key pair if you don’t already have one. Download the .pem file and store it somewhere safe. You cannot re-download it once the dialog closes.
The part that catches people is the security group. The default only opens port 22. Your API needs two more inbound rules:
| Port | Protocol | Source | Purpose |
|---|---|---|---|
| 22 | TCP | Your IP address | SSH access |
| 80 | TCP | 0.0.0.0/0 | HTTP traffic from the internet |
| 443 | TCP | 0.0.0.0/0 | HTTPS (when you add TLS later) |
Restricting port 22 to your IP address is worth doing from the start. Opening SSH to the world generates a constant stream of automated brute-force attempts that fill your auth logs with noise and nothing else.
Once the instance launches, wait for the status check to move from “Initializing” to “2/2 checks passed.” After that, the instance is ready for connections.
SSH in and install Node.js
Find your instance in the EC2 console, copy the public DNS address from the instance details panel, then connect:
chmod 400 your-key.pem
ssh -i your-key.pem ec2-user@ec2-xx-xx-xx-xx.compute-1.amazonaws.com
Amazon Linux uses ec2-user as the default login. You’ll see a brief welcome banner once the connection opens.
Install Node.js using NVM (Node Version Manager). NVM handles version switching cleanly and doesn’t require sudo for global package installs:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts
node --version
npm --version
nvm install --lts pulls the current Long Term Support release. Verify both commands print version numbers before moving on — if either fails, the install script likely didn’t modify your shell profile correctly, and sourcing it again usually fixes it.
Check this before moving on
-
node --versionprints a version number -
npm --versionprints a version number - You are connected as
ec2-user
Keep the API running with PM2
Starting your app with node index.js ties the process to your SSH session. It exits the moment you log out. PM2 solves this: it runs your app as a background daemon and restarts it automatically if it crashes.
Install PM2 globally:
npm install -g pm2
Get your project onto the server. If it lives in a repository:
git clone https://github.com/your-username/your-api.git
cd your-api
npm install --omit=dev
Using --omit=dev skips development dependencies and keeps the installed footprint smaller on the production server. Start the API:
pm2 start index.js --name my-api
Then configure PM2 to restore your processes after a server reboot:
pm2 startup
PM2 prints a command that requires root privileges — copy it and run it exactly as shown. After that, save the current process list:
pm2 save
Run pm2 status to confirm the app shows online. If it shows errored instead, run pm2 logs my-api to see the failure output.
The pm2 startup + pm2 save combination is what most people skip the first time. If the server reboots and the API is down, those two commands are almost always the missing piece.
Put nginx in front of your API
Your Node.js app is listening on port 3000 (or whichever port you chose). Port 3000 is not in your security group’s inbound rules, and it shouldn’t be. Port 80 is. nginx bridges the gap: it listens on port 80 and forwards incoming requests to your Node.js process.
Install nginx and configure it to start on boot:
sudo yum install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Create a new nginx configuration file for your API:
sudo nano /etc/nginx/conf.d/my-api.conf
Add this server block:
server {
listen 80 default_server;
listen [::]:80 default_server;
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;
}
}
The proxy_set_header lines pass the original request details through to Node.js. Without them, your API sees only requests from localhost and loses the original Host header, which matters once you add domain-based routing or logging.
If the default server {} block inside /etc/nginx/nginx.conf also listens on port 80, comment it out — two default_server blocks on the same port will cause nginx to reject the configuration.
Test the configuration before applying it:
sudo nginx -t
If the output shows syntax is ok and test is successful, reload:
sudo systemctl reload nginx
Open a browser and visit http://<your-instance-ip>. If your API has a route at /, you’ll see the response.
Assign an Elastic IP so the address sticks
Every time you stop and start an EC2 instance, its public IP address changes. DNS records, API clients, and anything else that references the IP all break when it does. An Elastic IP is a static public IPv4 address that remains fixed regardless of how many times the instance restarts.
In the EC2 console, go to “Network & Security” → “Elastic IPs.” Click “Allocate Elastic IP address,” confirm “Amazon’s pool of IPv4 addresses,” then select the newly allocated address and choose “Actions” → “Associate Elastic IP address.” Pick your instance from the dropdown and confirm.
Your instance now has a permanent public address. If you have a domain, update its A record to point to this IP.
One cost detail worth knowing: Elastic IP addresses are billed whether they’re attached to a running instance or sitting idle. The default quota is five per region. If you delete an instance and forget to release its Elastic IP, the charge continues. Release unused addresses from the “Elastic IPs” page when you’re done with them.
What breaks and how to spot it
A handful of problems come up reliably on a first EC2 deployment.
The security group is blocking traffic. If curl http://localhost responds from inside the instance but the browser can’t reach it, the security group is the issue. Open the “Inbound rules” tab on the security group attached to your instance and confirm port 80 exists with source 0.0.0.0/0.
PM2 processes didn’t survive the reboot. If pm2 status shows no processes after a restart, the startup script wasn’t registered. Start your app again, run pm2 save, and verify the pm2 startup command was executed with root privileges. Running pm2 startup generates the command — it doesn’t register it. You have to run the printed command separately.
nginx won’t reload after a config edit. A missing semicolon or an unclosed brace in nginx.conf or any included file causes nginx to refuse to reload. Run sudo nginx -t before every reload. The error output shows the exact file and line number.
The Node.js process is crashing on startup. If PM2 shows errored status or the restart counter keeps climbing, the app is failing immediately. Run pm2 logs my-api --lines 50 to see recent output. The most common causes are a missing environment variable, a wrong port, or a missing node_modules directory.
| Symptom | Likely cause | First check |
|---|---|---|
| Browser hangs, no response | Port 80 not open | Security group inbound rules |
502 Bad Gateway |
Node.js not running | pm2 status, pm2 logs my-api |
| API down after reboot | PM2 startup not configured | pm2 startup + run the printed command + pm2 save |
| nginx won’t reload | Config syntax error | sudo nginx -t |
403 Forbidden from nginx |
Conflicting default server block | Comment out default server {} in nginx.conf |
The API that survives a reboot
The shift this setup makes is worth naming. node index.js in a terminal is a shortcut for local development. PM2 under nginx is infrastructure — the API runs whether you’re logged in or not, whether it crashes or not, whether the server restarts at 3am or not.
The four pieces are deliberately layered. EC2 provides the compute. The security group controls what traffic reaches it. nginx handles incoming HTTP and proxies it to your app’s port. PM2 ensures Node.js is always running and restores the process list on every boot.
The natural next step from here is HTTPS. Point your domain’s A record to the Elastic IP, then use Certbot to get a free TLS certificate from Let’s Encrypt. The nginx config gains a server_name directive, the certificate file paths, and a redirect from port 80 to 443. But that’s its own complete topic — get HTTP working reliably before adding TLS to the stack.