Skip to main content

Command Palette

Search for a command to run...

Building a Secure Production Backend on AWS EC2 with CI/CD, HTTPS & PM2

Published
4 min read

In this blog, I’ll walk through how I deployed my Node.js backend on AWS EC2, configured Nginx, managed processes with PM2, secured the API using HTTPS (Let’s Encrypt), and finally connected it to a custom domain.

This wasn’t smooth — I ran into SSH issues, mixed content errors, missing packages, DNS delays, and broken deployments. But that’s exactly why this experience mattered.

Tech Stack Used

Here’s what I used for this deployment:

  • Backend: Node.js + Express

  • Database: PostgreSQL

  • Cloud Provider: AWS

  • Server: EC2 (Ubuntu)

  • Process Manager: PM2

  • Reverse Proxy: Nginx

  • SSL: Let’s Encrypt (Certbot)

  • CI/CD: GitHub Actions

  • Domain & DNS: Hostinger

  • Version Control: Git & GitHub

Architecture diagram

Step 1: Launching an EC2 Instance

I started by launching an EC2 instance on AWS using Ubuntu.

Key decisions:

  • Instance type suitable for backend workloads (free-tier friendly)

  • Created a key pair (.pem file) for SSH access

  • Configured security groups to allow:

    • SSH (22)

    • HTTP (80)

    • HTTPS (443)

    • Backend port (initially 5000)

Security groups act like a firewall. Without opening the correct ports, nothing works — learned this the hard way.

Step 2: Connecting to the Server via SSH

Using the .pem key, I connected to the server:

ssh -i "filename.pem" ubuntu@<EC2_PUBLIC_IP>

The first time you connect, AWS asks you to trust the server’s fingerprint — this is normal and ensures you’re connecting to the right machine.

Step 3: Setting Up the Backend Environment

Installing Node.js (via NVM)

Instead of installing Node directly, I used NVM, which makes version management easier:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
nvm install 20
nvm use 20

This step is critical — missing Node or NPM caused multiple deployment failures later when CI/CD ran.

Step 4: Running the Backend with PM2

Once the backend code was on the server, I used PM2 to keep the app running even after crashes or reboots.

npm install -g pm2
pm2 start server.js --name "backend"
pm2 save

PM2 acts like a supervisor. Without it, your backend dies the moment your SSH session closes.

Step 5: Using Nginx as a Reverse Proxy

Exposing the backend directly on port 5000 isn’t ideal.
Instead, I configured Nginx to act as a reverse proxy:

Client → Nginx (80/443) → Node.js (5000)

This gives:

  • Cleaner URLs

  • Better security

  • HTTPS support

  • Easier scaling later

Once Nginx was set up, I could access my API via a domain instead of an IP + port.

Step 6: Setting Up a Custom Domain

I bought a domain and created an A record:

NameValue (IP)
apiEC2 Public IP

DNS takes time to propagate, so initially ping failed — this is expected. Waiting patiently fixed it.

Step 7: Enabling HTTPS with Let’s Encrypt

This was one of the most satisfying steps.

Using Certbot, I generated a free SSL certificate:

sudo certbot --nginx -d api.caterview.online

After this:

  • HTTPS was enabled

  • Certificates were auto-renewable

  • Browser warnings disappeared

Seeing this message felt unreal:

“Congratulations! You have successfully enabled HTTPS.”

Step 8: Fixing Mixed Content Errors

At one point, my frontend (HTTPS) was calling an HTTP backend, which caused:

blocked:mixed-content

The fix was simple but important:

  • Make all backend URLs HTTPS

  • Update frontend environment variables

Security is only as strong as its weakest link.

Step 9: Automating Deployment with GitHub Actions

Manually pulling code every time was inefficient, so I set up CI/CD.

Whenever I push to main:

  • GitHub Actions SSHs into EC2

  • Pulls latest code

  • Installs dependencies

  • Restarts the backend via PM2

This made deployments fast, consistent, and repeatable.

Common Issues I Faced (and Learned From)

  • SSH timeouts due to security group misconfiguration

  • npm: command not found (Node not installed on EC2)

  • pm2: command not found (global install missing)

  • DNS not resolving immediately

  • Mixed content errors between frontend & backend

Every issue forced me to understand why things work, not just how.

Final Result

  • Backend running on AWS EC2

  • Secure HTTPS endpoint

  • Managed by PM2

  • Accessible via custom domain

  • Auto-deployed using CI/CD

This felt like a real engineering milestone.

Conclusion

Deploying this backend taught me more than dozens of tutorials ever could.
It pushed me beyond writing code into owning software in production.

If you’re a student or fresher — don’t stop at localhost.
Deploy something. Break it. Fix it. Learn from it.

That’s how you grow.