Building a Secure Production Backend on AWS EC2 with CI/CD, HTTPS & PM2
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:
| Name | Value (IP) |
| api | EC2 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.
