How to Deploy a Node.js Application on AWS EC2 with Nginx and HTTPS
How to Deploy a Node.js Application on AWS EC2 with Nginx and HTTPS
Deploying a Node.js application is straightforward during local development. You install Node.js, run your application, and access it through localhost. However, making that application available securely on the internet requires additional infrastructure and configuration.
Amazon EC2 provides virtual servers where you can host your application. Nginx acts as a reverse proxy in front of Node.js, PM2 keeps the application process running, and Let's Encrypt provides a free SSL/TLS certificate for HTTPS.
In this practical tutorial, you will deploy a Node.js application on an Ubuntu EC2 instance, connect a custom domain, configure Nginx, enable HTTPS, and prepare the server for routine production maintenance.
- A Node.js application running on an AWS EC2 Ubuntu server.
- PM2 process management with automatic startup after reboot.
- Nginx reverse proxy listening on public HTTP and HTTPS ports.
- A custom domain pointing to your EC2 Elastic IP.
- A free Let's Encrypt TLS certificate with automatic renewal.
Table of Contents
- Understand the deployment architecture
- Prerequisites and requirements
- Launch an AWS EC2 instance
- Connect to EC2 through SSH
- Update Ubuntu and install essential packages
- Install Node.js
- Deploy the Node.js application
- Configure PM2
- Install and configure Nginx
- Connect your domain to EC2
- Enable HTTPS with Let's Encrypt
- Secure your deployment
- Deploy application updates
- Troubleshooting common errors
- Production deployment checklist
- Frequently asked questions
1. Understand the AWS EC2 Deployment Architecture
Before executing commands, understand how a request travels from a visitor's browser to your Node.js application.
When someone visits your website, the browser connects to Nginx through HTTP or HTTPS. Nginx receives the request and forwards it to the Node.js process running on an internal port, such as 3000. PM2 manages that Node.js process and helps restart it if it crashes.
Internet / User Browser
|
|
Domain: example.com
|
v
AWS EC2 Public IP
|
v
Nginx Web Server
Port 80 / 443
|
HTTPS termination
|
v
Reverse Proxy (Nginx)
|
v
Node.js Application
Port 3000
|
v
PM2 Process
|
v
Express.js API
What does each component do?
| Component | Responsibility |
|---|---|
| AWS EC2 | Provides the virtual machine that hosts your application. |
| Ubuntu | Operating system running on the EC2 instance. |
| Node.js | JavaScript runtime for executing the application. |
| Express.js | Web framework used for HTTP routes and APIs. |
| PM2 | Manages Node.js processes and application restarts. |
| Nginx | Handles incoming web traffic and forwards requests to Node.js. |
| Let's Encrypt | Issues free TLS certificates for HTTPS. |
This is a single-server deployment architecture. It is suitable for learning, small applications, prototypes, and some low-traffic production workloads. It is not automatically highly available or fault tolerant.
2. Prerequisites and Requirements
Prepare the following before starting:
- An active AWS account with permission to launch EC2 instances.
- A domain name that you control.
- A Node.js application stored in a Git repository.
- Basic knowledge of Linux commands and SSH.
- An SSH key pair for connecting to the EC2 instance.
- Access to your domain registrar or DNS provider.
Example environment
| Setting | Example |
|---|---|
| Cloud provider | Amazon Web Services |
| Compute service | Amazon EC2 |
| Operating system | Ubuntu Server 24.04 LTS |
| Application runtime | Node.js LTS |
| Application framework | Express.js |
| Process manager | PM2 |
| Web server | Nginx |
| Application port | 3000 |
| Web ports | 80 and 443 |
| SSL provider | Let's Encrypt |
3. Launch an AWS EC2 Instance
First, create a virtual server using Amazon EC2.
Step 1: Open the EC2 dashboard
- Sign in to the AWS Management Console.
- Search for EC2.
- Open the EC2 dashboard.
- Choose Launch instance.
Step 2: Configure the instance
Use the following configuration as a starting point:
| Configuration | Recommended starting choice |
|---|---|
| Name | nodejs-production-server |
| AMI | Ubuntu Server 24.04 LTS |
| Architecture | 64-bit x86 or a compatible ARM build |
| Instance type | A small instance for testing; size production based on measured workload |
| Key pair | Create or select an existing key pair |
| Storage | Encrypted EBS volume with sufficient capacity |
The exact instance size depends on application memory usage, CPU demand, traffic, background jobs, and database requirements. Do not assume that a small instance is sufficient for every production application.
Step 3: Configure the security group
A security group acts as a virtual firewall for your EC2 instance. Add only the inbound rules your deployment needs.
| Type | Port | Source | Purpose |
|---|---|---|---|
| SSH | 22 | Your public IP /32 | Server administration |
| HTTP | 80 | 0.0.0.0/0 | Web traffic and certificate validation |
| HTTPS | 443 | 0.0.0.0/0 | Secure web traffic |
Do not create a public inbound rule for port 3000. Nginx will communicate with Node.js locally on the EC2 instance.
Step 4: Launch the instance
Review the configuration and click Launch instance. Wait until the instance reaches the running state and its status checks pass.
Step 5: Allocate an Elastic IP address
A normal public IPv4 address can change after an instance is stopped and started. A stable public address is useful when configuring DNS records.
- Open EC2 in the AWS Console.
- Navigate to Network & Security → Elastic IPs.
- Choose Allocate Elastic IP address.
- Allocate the address and associate it with your EC2 instance.
- Record the Elastic IP address for your DNS configuration.
4. Connect to EC2 Through SSH
After the instance is running, connect from your local terminal. Downloaded private key files should be protected and never committed to Git.
On Linux or macOS, update the key file permissions:
chmod 400 your-key.pem
Connect using the instance's Elastic IP:
ssh -i your-key.pem ubuntu@YOUR_ELASTIC_IP
Replace your-key.pem and YOUR_ELASTIC_IP with your actual values.
If the connection succeeds, you will see the Ubuntu server terminal. You are now connected to your EC2 instance.
5. Update Ubuntu and Install Essential Packages
Before installing Node.js, update the operating system package index and apply available package updates.
sudo apt update
sudo apt upgrade -y
Install commonly used tools:
sudo apt install -y curl git build-essential nginx
Verify that Nginx is installed and running:
sudo systemctl status nginx
Enable Nginx to start automatically when the server boots:
sudo systemctl enable nginx
At this stage, open your Elastic IP in a browser using HTTP:
http://YOUR_ELASTIC_IP
You should see the default Nginx welcome page if the security group allows port 80 and the service is running.
6. Install Node.js on Ubuntu
Node.js is required to run your backend application. For a maintainable deployment, use a supported Node.js LTS release compatible with your application's dependencies.
One convenient approach is to install Node.js using NVM (Node Version Manager). NVM allows you to manage Node.js versions for the deployment user.
Step 1: Install NVM
Use the official NVM installation instructions to obtain the current release command. The following is the standard installation pattern; verify the release tag against the official project before running it.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
Load NVM into the current shell:
source ~/.bashrc
Verify the installation:
nvm --version
Step 2: Install Node.js LTS
nvm install --lts
nvm use --lts
nvm alias default 'lts/*'
Check the installed versions:
node --version
npm --version
Using the LTS release helps keep the runtime on a supported release line. Before upgrading an existing production application, check your framework, native modules, and dependency compatibility.
7. Deploy the Node.js Application
Now deploy your Node.js application to the EC2 server. This example assumes that you already have an Express application in a Git repository.
Step 1: Clone the repository
Create an application directory:
mkdir -p ~/apps
cd ~/apps
Clone your repository:
git clone https://github.com/YOUR_USERNAME/YOUR_REPOSITORY.git node-api
cd node-api
Replace the repository URL with your actual Git repository. For private repositories, use a secure authentication method such as a deploy key or a short-lived access token. Do not embed credentials in a clone URL or commit them to source control.
Step 2: Install dependencies
If your project contains a package-lock.json file, install the exact dependency versions recorded in the lockfile:
npm ci
For TypeScript or build-based projects, execute the build script defined in your package.json:
npm run build
Only run this command if your project defines a build script.
Step 3: Create an Express application for testing
If you do not have an application ready, create a simple Express API to verify the deployment.
npm init -y
npm install express
Create an app.js file:
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.json({
message: "Node.js application deployed successfully!",
environment: process.env.NODE_ENV || "development"
});
});
app.get("/health", (req, res) => {
res.status(200).json({
status: "healthy",
timestamp: new Date().toISOString()
});
});
app.listen(PORT, "127.0.0.1", () => {
console.log(`Server running on port ${PORT}`);
});
This example binds the application to the loopback interface. That means it accepts connections from Nginx on the same server rather than exposing port 3000 directly to the public network.
Step 4: Test the application locally
Start the application:
node app.js
Open another SSH session and test the health endpoint:
curl http://127.0.0.1:3000/health
You should receive a JSON response with a healthy status. Stop the foreground process with Ctrl+C before configuring PM2.
8. Configure PM2 to Manage the Node.js Process
Running an application directly with node app.js is not a good process-management strategy for a server. The process stops when the terminal session ends, and you must manually restart it after a crash or reboot.
PM2 is a process manager that can keep Node.js applications running in the background, restart them after failures, and integrate with system startup.
Step 1: Install PM2
npm install -g pm2
Verify the installation:
pm2 --version
Step 2: Start the application
From your project directory, start the application with PM2:
cd ~/apps/node-api
pm2 start app.js --name node-api
Check the application status:
pm2 status
View application logs:
pm2 logs node-api
Step 3: Configure PM2 startup
PM2 can generate a startup command for the operating system. Run:
pm2 startup
PM2 will display a command that must be executed with elevated privileges. Copy and run the exact command it provides. The generated command may include the path to the Node.js binary managed by NVM and the current user.
Then save the process list:
pm2 save
Useful PM2 commands
| Command | Purpose |
|---|---|
pm2 status | View managed processes. |
pm2 logs node-api | View application logs. |
pm2 restart node-api | Restart the application. |
pm2 reload node-api | Request a reload; behavior depends on application and cluster configuration. |
pm2 stop node-api | Stop the process. |
pm2 delete node-api | Remove the process from PM2. |
9. Install and Configure Nginx as a Reverse Proxy
Nginx is a high-performance web server that can receive public requests and forward them to your Node.js application.
In this architecture, users connect to Nginx through ports 80 and 443. Nginx forwards the requests to the Express application running on 127.0.0.1:3000.
Step 1: Confirm Nginx is running
sudo systemctl status nginx
If it is not running, start and enable it:
sudo systemctl enable --now nginx
Step 2: Create an Nginx server block
Create a configuration file for your domain:
sudo nano /etc/nginx/sites-available/node-api
Add the following configuration. Replace example.com with your actual domain.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Step 3: Enable the configuration
Create a symbolic link to enable the server block:
sudo ln -s /etc/nginx/sites-available/node-api /etc/nginx/sites-enabled/node-api
Remove the default site link if it conflicts with your intended configuration:
sudo rm -f /etc/nginx/sites-enabled/default
Test the Nginx configuration:
sudo nginx -t
If the syntax test succeeds, reload Nginx:
sudo systemctl reload nginx
Step 4: Test the reverse proxy
Before enabling HTTPS, test the domain over HTTP once DNS is configured:
curl -I http://example.com
You should receive an HTTP response from your application through Nginx. If you see a 502 Bad Gateway response, check whether PM2 is running and whether the application is listening on the expected local port.
10. Connect Your Domain to AWS EC2
To access your application through a domain name, configure DNS records with your domain registrar or DNS provider.
Assume your domain is example.com and your EC2 Elastic IP is YOUR_ELASTIC_IP.
Step 1: Create an A record
| Record type | Host / Name | Value |
|---|---|---|
| A | @ | YOUR_ELASTIC_IP |
| A | www | YOUR_ELASTIC_IP |
The exact host field format depends on your DNS provider. Some providers use @ for the root domain; others represent it differently.
Step 2: Verify DNS resolution
Use the following commands from your local computer or EC2 server:
dig +short example.com
dig +short www.example.com
Both should resolve to the intended public IP address. DNS changes can take time to propagate depending on TTL and resolver caching.
11. Enable HTTPS with Let's Encrypt and Certbot
HTTPS encrypts traffic between the browser and your web server. It protects data in transit and helps users verify that they are communicating with the intended domain.
Let's Encrypt is a certificate authority that provides free domain-validated TLS certificates. Certbot is a client that can request and install certificates and configure supported web servers.
Step 1: Install Certbot
For Ubuntu, follow the current Certbot installation instructions for your release. The package-based approach below installs Certbot and its Nginx integration:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
Check the installed version:
certbot --version
Step 2: Request and install the certificate
Run Certbot with the Nginx plugin. Include only the domain names that have been configured in DNS and in your Nginx server block.
sudo certbot --nginx -d example.com -d www.example.com
Follow the interactive instructions. Certbot will validate domain ownership and can update the Nginx configuration to enable HTTPS.
If you do not use the www hostname, request only the root domain:
sudo certbot --nginx -d example.com
Step 3: Verify HTTPS
Open your domain in a browser:
https://example.com
You should see a secure connection indicator in the browser. You can also test the response from the terminal:
curl -I https://example.com
Step 4: Test automatic certificate renewal
Let's Encrypt certificates have a limited validity period. Certbot installations commonly configure a renewal timer or scheduled task. Verify that automatic renewal is configured.
sudo certbot renew --dry-run
A successful dry run indicates that the renewal process can complete in the test environment. Also inspect the timer on Ubuntu:
systemctl list-timers | grep certbot
If your installation uses a different renewal mechanism, inspect the corresponding systemd service or scheduled task. Do not assume renewal is working merely because the first certificate was issued successfully.
12. Secure Your Node.js Deployment
A working deployment is not necessarily a secure deployment. Production systems need ongoing attention to network exposure, operating system updates, application secrets, permissions, and monitoring.
12.1 Keep application ports private
Only expose the public ports required by your web server. The Node.js application should listen on the loopback interface or a private network interface and should not have a public security group rule for port 3000.
12.2 Restrict SSH access
Limit SSH access to trusted administrator IP addresses. For teams, consider AWS Systems Manager Session Manager to reduce direct SSH exposure. Protect private keys and rotate access according to your organization's security requirements.
12.3 Use environment variables for secrets
Do not commit passwords, database credentials, API keys, or private tokens to Git. Store production configuration outside source control and restrict file permissions.
For example, create a protected environment file:
mkdir -p ~/apps/node-api/config
nano ~/apps/node-api/config/.env
Example configuration:
NODE_ENV=production
PORT=3000
DATABASE_URL=YOUR_DATABASE_CONNECTION_STRING
Restrict access to the file:
chmod 600 ~/apps/node-api/config/.env
Use a suitable configuration loader in your application and configure PM2 to load the environment file through your deployment process. For production secrets, consider AWS Secrets Manager or Systems Manager Parameter Store, with access granted through a least-privilege EC2 instance role.
12.4 Keep Ubuntu and dependencies updated
Apply security updates regularly and review dependency vulnerabilities:
sudo apt update
sudo apt upgrade -y
npm audit
Review the audit output before applying dependency changes. Test updates in a staging environment before deploying them to production.
12.5 Configure application logging and monitoring
PM2 provides basic process and log visibility:
pm2 status
pm2 logs node-api
pm2 monit
For AWS infrastructure monitoring, use Amazon CloudWatch to collect EC2 metrics and configure alarms for important conditions such as sustained CPU utilization, status check failures, and low disk space through an appropriate agent or monitoring solution.
12.6 Configure backups
Use encrypted EBS snapshots or an appropriate AWS backup strategy for important server data. Keep application source code in version control and store databases and uploaded files in services designed for persistent data rather than relying only on the EC2 root volume.
12.7 Consider an application firewall and rate limiting
For public APIs, implement request validation, authentication, authorization, rate limiting, and appropriate security headers. For more advanced edge protection, evaluate AWS WAF with a suitable front-end architecture such as an Application Load Balancer or CloudFront.
13. Deploy Application Updates
Once your application is live, you will need a repeatable process for releasing new versions.
For a basic Git-based deployment, connect to the server and move to the application directory:
cd ~/apps/node-api
Fetch and apply the tested release from your deployment branch:
git pull origin main
npm ci
If the project has a build step, run it:
npm run build
Restart the PM2 process:
pm2 restart node-api
pm2 save
Verify the application:
pm2 status
curl http://127.0.0.1:3000/health
curl -I https://example.com
git pull as a complete release strategy. Use tested commits, dependency lockfiles, environment separation, health checks, backups, and a rollback plan. A zero-downtime deployment requires additional design, such as multiple application processes behind a load balancer or a carefully configured cluster strategy.
14. Troubleshooting Common AWS EC2 Deployment Errors
Deployment problems often come from security group rules, DNS configuration, Nginx syntax, application process failures, or certificate validation.
| Problem | Possible cause | What to check |
|---|---|---|
| SSH connection timeout | Port 22 blocked or incorrect source IP | Security group, route table, public IP and SSH settings. |
| Website not reachable | Port 80 or 443 blocked | Inbound security group rules and Nginx service. |
| 502 Bad Gateway | Node.js process stopped or wrong proxy port | PM2 status, app logs and Nginx proxy_pass. |
| Domain not resolving | Incorrect or incomplete DNS records | A records, Elastic IP and DNS propagation. |
| Certbot validation failure | DNS mismatch or port 80 unreachable | Domain records, Nginx configuration and security group. |
| Application stops after reboot | PM2 startup not configured correctly | PM2 startup command, saved process list and service status. |
| Permission denied | Incorrect file ownership or permissions | SSH key permissions and application directory ownership. |
| Application out of memory | Instance has insufficient memory or a memory leak | CloudWatch metrics, system logs and application profiling. |
Useful diagnostic commands
Check Nginx service status:
sudo systemctl status nginx
Validate Nginx configuration:
sudo nginx -t
View Nginx error logs:
sudo tail -n 100 /var/log/nginx/error.log
View PM2 process status:
pm2 status
View application logs:
pm2 logs node-api --lines 100
Check which process is listening on port 3000:
sudo ss -lntp | grep 3000
Test the local application:
curl -i http://127.0.0.1:3000/health
Test the public HTTPS endpoint:
curl -Iv https://example.com
15. Production Deployment Checklist
Before announcing your application as production-ready, review the following checklist.
- EC2 instance is running with an appropriate size and supported Ubuntu release.
- Elastic IP is associated with the intended instance.
- SSH access is restricted to trusted administrators or managed through Session Manager.
- Only required inbound ports are open.
- Node.js is installed from a trusted source and uses a supported LTS release.
- Application dependencies are installed from the lockfile.
- Secrets are stored outside source control.
- Node.js listens on a private interface or loopback address.
- PM2 manages the application and startup configuration is saved.
- Nginx reverse proxy configuration passes its syntax test.
- DNS records point to the correct Elastic IP.
- HTTPS certificate is valid for the configured domain names.
- Certificate renewal dry run succeeds.
- Application logs and infrastructure monitoring are configured.
- Backups and a recovery plan exist for important data.
- AWS Budget alerts are configured to monitor costs.
16. Frequently Asked Questions
1. Can I deploy a Node.js application on AWS EC2 for free?
Possibly, depending on your AWS account's current Free Tier or promotional eligibility, instance choice, region, storage, public IPv4 usage, and data transfer. EC2 deployment is not automatically free. Always review the current AWS pricing and configure budget alerts.
2. Why should I use Nginx with Node.js?
Nginx provides a public web-server entry point and reverse proxy. It can handle incoming HTTP and HTTPS traffic, forward requests to Node.js, and manage TLS termination. This keeps the application process separate from the public-facing web server.
3. What is the purpose of PM2 in Node.js deployment?
PM2 manages Node.js processes. It can restart applications after crashes, run them in the background, expose logs and process status, and integrate with system startup.
4. Do I need a domain name to enable HTTPS?
For the standard Let's Encrypt HTTP-01 validation process used in this tutorial, you need a domain name that resolves to your server and is reachable for validation. A bare EC2 public IP is not sufficient for this domain-validated certificate workflow.
5. Is Let's Encrypt SSL free?
Yes. Let's Encrypt provides free domain-validated TLS certificates. You still need to configure certificate renewal and maintain the web server correctly.
6. Should I expose Node.js port 3000 publicly?
No. In this architecture, Nginx receives public traffic and forwards it to Node.js over the local loopback interface. Keep port 3000 closed to public inbound traffic.
7. Can I host multiple Node.js applications on one EC2 instance?
Yes. You can run applications on different local ports and configure separate Nginx server blocks for different domains or subdomains. However, monitor CPU, memory, storage, and network usage to avoid resource contention.
8. What happens when my EC2 instance restarts?
Nginx can start automatically when enabled through systemd. PM2 can restore saved application processes when its startup integration is configured correctly. Verify both services after a reboot rather than assuming they will work.
9. Is EC2 suitable for a high-traffic Node.js application?
EC2 can host high-traffic applications, but a single instance is a single point of failure. For higher availability, consider multiple EC2 instances across Availability Zones, an Application Load Balancer, Auto Scaling, centralized logs, and managed data services.
10. Can I use this deployment for a Next.js application?
Yes, with adjustments. A Next.js application can run as a Node.js server behind Nginx, but you must use the appropriate production build and start command. Static assets, image optimization, caching, and environment variables also need to be configured according to the application.
Conclusion
You have now learned how to deploy a Node.js application on AWS EC2 using Ubuntu, PM2, Nginx, a custom domain, and HTTPS with Let's Encrypt.
The deployment flow is straightforward: launch and secure an EC2 instance, install Node.js, deploy the application, configure PM2, route incoming requests through Nginx, point your domain to an Elastic IP, and enable HTTPS.
Remember that a successful deployment is only the starting point. Production applications also need monitoring, security updates, backups, cost controls, and a tested release and rollback process.
If you are learning AWS and DevOps, this project is a useful foundation before moving on to automated CI/CD pipelines, Docker, infrastructure as code, and container orchestration with Kubernetes.

Comments