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.

What you will build:
  • 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

 
How to Deploy a Node.js Application

 How to Deploy a Node.js Application on AWS EC2 with Nginx and HTTPS

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?

ComponentResponsibility
AWS EC2Provides the virtual machine that hosts your application.
UbuntuOperating system running on the EC2 instance.
Node.jsJavaScript runtime for executing the application.
Express.jsWeb framework used for HTTP routes and APIs.
PM2Manages Node.js processes and application restarts.
NginxHandles incoming web traffic and forwards requests to Node.js.
Let's EncryptIssues 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

SettingExample
Cloud providerAmazon Web Services
Compute serviceAmazon EC2
Operating systemUbuntu Server 24.04 LTS
Application runtimeNode.js LTS
Application frameworkExpress.js
Process managerPM2
Web serverNginx
Application port3000
Web ports80 and 443
SSL providerLet's Encrypt
Cost note: EC2, EBS storage, public IPv4 addresses, data transfer, and other AWS resources can incur charges. Check current pricing for your selected region and instance type. Set an AWS Budget alert before launching resources. Free Tier eligibility and allowances depend on your account and current AWS terms.

3. Launch an AWS EC2 Instance

First, create a virtual server using Amazon EC2.

Step 1: Open the EC2 dashboard

  1. Sign in to the AWS Management Console.
  2. Search for EC2.
  3. Open the EC2 dashboard.
  4. Choose Launch instance.

Step 2: Configure the instance

Use the following configuration as a starting point:

ConfigurationRecommended starting choice
Namenodejs-production-server
AMIUbuntu Server 24.04 LTS
Architecture64-bit x86 or a compatible ARM build
Instance typeA small instance for testing; size production based on measured workload
Key pairCreate or select an existing key pair
StorageEncrypted 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.

TypePortSourcePurpose
SSH22Your public IP /32Server administration
HTTP800.0.0.0/0Web traffic and certificate validation
HTTPS4430.0.0.0/0Secure web traffic
Important security rule: Never expose SSH port 22 to the entire internet using 0.0.0.0/0 for routine administration. Restrict it to your trusted IP address or use AWS Systems Manager Session Manager with the required IAM permissions and network configuration.

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.

  1. Open EC2 in the AWS Console.
  2. Navigate to Network & Security → Elastic IPs.
  3. Choose Allocate Elastic IP address.
  4. Allocate the address and associate it with your EC2 instance.
  5. Record the Elastic IP address for your DNS configuration.
AWS may charge for public IPv4 addresses, including Elastic IP usage, according to its current pricing rules. Check the pricing page and release addresses you no longer need.

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.

If SSH reports a timeout, check the security group, your current public IP, the instance's public connectivity, and the subnet route table. If you receive a permission error for the key, check its file permissions and the SSH username.

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
Result: PM2 can now restore the saved application process list when the server boots, provided the generated startup service is correctly installed and the application files remain available.

Useful PM2 commands

CommandPurpose
pm2 statusView managed processes.
pm2 logs node-apiView application logs.
pm2 restart node-apiRestart the application.
pm2 reload node-apiRequest a reload; behavior depends on application and cluster configuration.
pm2 stop node-apiStop the process.
pm2 delete node-apiRemove 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.

Why use Nginx? It provides a public entry point, reverse proxying, TLS termination, request handling, and a convenient place to configure redirects and security headers. Keep your Node.js application port private.

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 typeHost / NameValue
A@YOUR_ELASTIC_IP
AwwwYOUR_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.

Before requesting an SSL certificate: Confirm that both domain names resolve to your EC2 public IP, that inbound TCP port 80 is allowed, and that Nginx is serving the correct domain configuration. If you do not use the www hostname, remove it from the Nginx configuration and certificate request.

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.

HTTPS enabled: Your Node.js application can now be accessed through your custom domain using an encrypted connection, provided the certificate covers the hostname being visited.

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.

Never put real secrets in screenshots, public repositories, blog examples, or deployment logs. The values above are placeholders, not working credentials.

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
For production, avoid treating a direct 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.

ProblemPossible causeWhat to check
SSH connection timeoutPort 22 blocked or incorrect source IPSecurity group, route table, public IP and SSH settings.
Website not reachablePort 80 or 443 blockedInbound security group rules and Nginx service.
502 Bad GatewayNode.js process stopped or wrong proxy portPM2 status, app logs and Nginx proxy_pass.
Domain not resolvingIncorrect or incomplete DNS recordsA records, Elastic IP and DNS propagation.
Certbot validation failureDNS mismatch or port 80 unreachableDomain records, Nginx configuration and security group.
Application stops after rebootPM2 startup not configured correctlyPM2 startup command, saved process list and service status.
Permission deniedIncorrect file ownership or permissionsSSH key permissions and application directory ownership.
Application out of memoryInstance has insufficient memory or a memory leakCloudWatch 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.

Next step: Explore more AWS, Node.js, and DevOps tutorials on CloudDevOpsTechLab to build practical cloud engineering skills.

References and Official Documentation

Comments

Popular posts from this blog

AWS Cost Optimization: 15 Practical Ways to Reduce Your Monthly Cloud Bill

AWS IAM Roles vs IAM Users: Differences, Security, and Best Practices