Skip to content

Instantly share code, notes, and snippets.

@adarsh-chakraborty
Created August 14, 2026 05:35
Show Gist options
  • Select an option

  • Save adarsh-chakraborty/37b63ab85a606eec5a98fa06512f3649 to your computer and use it in GitHub Desktop.

Select an option

Save adarsh-chakraborty/37b63ab85a606eec5a98fa06512f3649 to your computer and use it in GitHub Desktop.

EC2 Production Deployment Guide

Ubuntu + Node.js LTS + PM2 + Caddy

This guide covers a basic production deployment of a Node.js application on an AWS EC2 instance.

Stack

  • AWS EC2
  • Ubuntu 24.04 LTS
  • Node.js 24.x LTS
  • npm
  • PM2
  • Caddy
  • Git
  • HTTPS with automatic TLS
  • Optional UFW firewall

Node.js version: As of August 2026, Node.js 24.x is the current LTS line. This guide uses Node.js 24 LTS rather than the non-LTS Current release for production.


1. EC2 Prerequisites

Create an EC2 instance with:

  • Ubuntu 24.04 LTS
  • At least 2 GB RAM for a small application
  • Elastic IP recommended for production
  • SSH key configured

Recommended Security Group

Allow inbound:

Type Port Source


SSH 22 Your IP only HTTP 80 0.0.0.0/0 HTTPS 443 0.0.0.0/0

Do not expose your Node.js application port such as 3000 publicly if Caddy is going to reverse proxy to it.


2. Connect to EC2

From your local machine:

ssh -i your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP

Example:

ssh -i production.pem ubuntu@54.123.123.123

3. Update the Server

sudo apt update
sudo apt upgrade -y

Install basic utilities:

sudo apt install -y git curl unzip build-essential

Verify:

git --version
curl --version

4. Create a Deployment User (Optional but Recommended)

For a simple deployment, you can use the default ubuntu user.

For a more controlled production setup, create a dedicated user:

sudo adduser deploy

Give it sudo access if required:

sudo usermod -aG sudo deploy

For a simple setup, you can continue using ubuntu.


5. Install Node.js LTS

The recommended approach is to use NVM (Node Version Manager). This makes it easy to change Node.js versions later without modifying the system packages.

Install NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

Reload your shell:

source ~/.bashrc

Verify NVM:

nvm --version

Install Node.js 24 LTS

nvm install 24

Make Node.js 24 the default:

nvm alias default 24
nvm use 24

Verify:

node -v
npm -v

Expected Node.js version:

v24.x.x

6. Install PM2

PM2 keeps your Node.js application running in production and automatically restarts it if the process crashes.

Install:

npm install -g pm2

Verify:

pm2 -v

7. Clone Your Application

Create an application directory:

mkdir -p ~/apps
cd ~/apps

Clone your repository:

git clone YOUR_GIT_REPOSITORY_URL

Example:

git clone git@gitlab.com:company/my-app.git

Enter the project:

cd my-app

8. Install Dependencies

For production:

npm ci

If your project does not have a package-lock.json, use:

npm install

9. Configure Environment Variables

Create your production environment file:

nano .env

Example:

NODE_ENV=production
PORT=3000

DATABASE_URL=your_database_url
REDIS_URL=your_redis_url
API_URL=https://api.example.com

Save the file.

Make sure .env is not committed to Git:

echo ".env" >> .gitignore

If the repository already contains .env.example, use it as a reference:

cp .env.example .env
nano .env

10. Build the Application

For a typical Node.js application:

npm run build

For a Next.js application:

npm run build

Test the application manually:

npm start

If the application starts successfully, stop it with:

CTRL + C

11. Start the Application with PM2

For a Node.js application whose production command is npm start:

pm2 start npm --name "my-app" -- start

If your application requires a specific port:

PORT=3000 pm2 start npm --name "my-app" -- start

Check the process:

pm2 status

View logs:

pm2 logs my-app

View only the last 100 lines:

pm2 logs my-app --lines 100

12. Test the Application Locally

If the application is running on port 3000:

curl http://localhost:3000

If you receive an HTTP response, your Node.js application is running correctly.

You can also check the port:

sudo ss -ltnp | grep 3000

13. Configure PM2 to Start After Reboot

Run:

pm2 startup

PM2 will print a command similar to:

sudo env PATH=$PATH:/home/ubuntu/.nvm/versions/node/v24.x.x/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu

Copy and run the exact command printed by PM2.

Then save the currently running applications:

pm2 save

Verify:

pm2 status

Now PM2 will restore your application after an EC2 reboot.


14. Install Caddy

Caddy will act as the reverse proxy and handle HTTPS.

Install the official Caddy repository:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

Add the Caddy signing key:

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | sudo gpg --dearmor \
  -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

Add the Caddy repository:

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | sudo tee /etc/apt/sources.list.d/caddy-stable.list

Update packages:

sudo apt update

Install Caddy:

sudo apt install -y caddy

Verify:

caddy version

Check the service:

sudo systemctl status caddy

Caddy's official Ubuntu package runs as a systemd service.


15. Configure Your Domain DNS

Before enabling HTTPS, point your domain/subdomain to the EC2 instance.

Example:

app.example.com

Create an A record:

Type:  A
Name:  app
Value: YOUR_EC2_ELASTIC_IP
TTL:   300

For example:

app.example.com -> 54.123.123.123

If using an Elastic IP, use the Elastic IP rather than the temporary EC2 public IP.

Verify DNS:

dig +short app.example.com

It should return your EC2 IP.


16. Configure Caddy

Open the Caddy configuration:

sudo nano /etc/caddy/Caddyfile

For a Node.js application running on port 3000:

app.example.com {
    reverse_proxy localhost:3000
}

Save the file.


17. Validate Caddy Configuration

Before reloading Caddy:

sudo caddy validate --config /etc/caddy/Caddyfile

You should see:

Valid configuration

18. Reload Caddy

sudo systemctl reload caddy

Check:

sudo systemctl status caddy

View Caddy logs:

sudo journalctl -u caddy -f

19. HTTPS

Once:

  1. DNS points to the EC2 instance
  2. Port 80 is publicly accessible
  3. Port 443 is publicly accessible
  4. Caddy is running

Caddy will automatically obtain and renew the TLS certificate for:

https://app.example.com

You do not need to manually create or renew Let's Encrypt certificates.

Test:

curl -I https://app.example.com

20. Optional: Enable UFW

If you want an additional firewall on the EC2 instance:

sudo apt install -y ufw

Allow SSH first:

sudo ufw allow 22/tcp

Allow HTTP:

sudo ufw allow 80/tcp

Allow HTTPS:

sudo ufw allow 443/tcp

Enable:

sudo ufw enable

Check:

sudo ufw status

Important: Always allow SSH before enabling UFW, otherwise you can lock yourself out of the server.


21. Recommended Application Architecture

The final architecture should look like:

                    Internet
                       |
                       |
                app.example.com
                       |
                       v
                +--------------+
                |    Caddy     |
                |    :80/:443  |
                +--------------+
                       |
                       | reverse proxy
                       v
                +--------------+
                |     PM2      |
                |   Node.js    |
                |    :3000     |
                +--------------+
                       |
                       v
              Application / APIs

The Node.js port (3000) should only be accessible locally.

Users should access:

https://app.example.com

not:

http://YOUR_EC2_IP:3000

22. Deployment After the Initial Setup

Once the server is configured, a basic deployment becomes:

cd ~/apps/my-app
git pull
npm ci
npm run build
pm2 restart my-app

Check:

pm2 status
pm2 logs my-app --lines 100

If the Caddy configuration was changed:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

23. Recommended Deployment Script

You can create a simple deployment script:

nano ~/deploy.sh

Add:

#!/bin/bash

set -e

APP_DIR="$HOME/apps/my-app"

echo "Starting deployment..."

cd "$APP_DIR"

echo "Pulling latest code..."
git pull

echo "Installing dependencies..."
npm ci

echo "Building application..."
npm run build

echo "Restarting application..."
pm2 restart my-app

echo "Deployment completed."

pm2 status

Make it executable:

chmod +x ~/deploy.sh

Run deployment:

~/deploy.sh

24. Useful PM2 Commands

List applications

pm2 status

Start application

pm2 start npm --name "my-app" -- start

Restart application

pm2 restart my-app

Stop application

pm2 stop my-app

Delete application

pm2 delete my-app

Logs

pm2 logs my-app

Monitor

pm2 monit

Save processes

pm2 save

25. Useful Caddy Commands

Check status

sudo systemctl status caddy

Restart Caddy

sudo systemctl restart caddy

Reload configuration

sudo systemctl reload caddy

Validate configuration

sudo caddy validate --config /etc/caddy/Caddyfile

View logs

sudo journalctl -u caddy -f

26. Troubleshooting

Application is not running

pm2 status
pm2 logs my-app

Check whether Node is listening:

sudo ss -ltnp | grep 3000

Caddy returns 502 Bad Gateway

Usually this means Caddy cannot reach your Node.js application.

Check:

pm2 status

Then:

curl http://localhost:3000

If this fails, fix the Node.js application first.


HTTPS certificate is not generated

Check DNS:

dig +short app.example.com

Check Caddy:

sudo journalctl -u caddy --no-pager -n 100

Make sure ports 80 and 443 are open in the EC2 Security Group.


Port 3000 is publicly accessible

Remove the inbound 3000 rule from the EC2 Security Group.

Your architecture should be:

Internet
   |
   +--> 80/443 --> Caddy --> localhost:3000 --> Node.js

Not:

Internet --> 3000 --> Node.js

27. Production Checklist

  • EC2 instance created
  • Elastic IP configured
  • SSH access working
  • Security Group configured
  • Ports 80 and 443 allowed
  • Node.js LTS installed
  • npm working
  • Git installed
  • Application cloned
  • .env configured
  • Dependencies installed
  • Application builds successfully
  • PM2 installed
  • Application running under PM2
  • PM2 startup configured
  • pm2 save completed
  • Caddy installed
  • DNS A record configured
  • Caddyfile configured
  • Caddy configuration validated
  • Caddy reloaded
  • HTTPS working
  • Application accessible through domain
  • Node.js application port not publicly exposed
  • Deployment script created (optional)

28. Final Setup

After completing everything, your server should have:

EC2
│
├── Ubuntu 24.04
│
├── Node.js 24 LTS
│
├── npm
│
├── PM2
│   └── my-app
│       └── localhost:3000
│
└── Caddy
    ├── :80
    ├── :443
    └── app.example.com

The production request flow is:

User
  |
  v
https://app.example.com
  |
  v
AWS EC2
  |
  v
Caddy :443
  |
  v
localhost:3000
  |
  v
Node.js Application

Official References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment