A step-by-step guide to deploying your first full-stack application with a real CI/CD pipeline, custom domain, and HTTPS,DevOps, Docker, AWS, Jenkins
By Dishan Sanjula ·https://dswijesooriya.is-a.dev/

Introduction
A few weeks ago, I finished building my first full-stack MERN application. It ran perfectly on localhost:5173, and I was genuinely proud of it.Then I asked myself the question every beginner eventually asks:

"Now what?"
I could have deployed it to Vercel or Netlify and called it done. And honestly — that's a perfectly valid choice. Those platforms are amazing, they're free, and they "just work."

But I wanted something different. I wanted to understand what actually happens under the hood. I wanted to know how production apps get deployed in real companies. I wanted to learn DevOps — that mysterious word everyone throws around but nobody explains properly to beginners.

So I decided to do something uncomfortable: I'd deploy my app on AWS EC2 with Docker, build a Jenkins CI/CD pipeline, set up Nginx as a reverse proxy, register a custom domain, and enable HTTPS with Let's Encrypt.

Total time: About 3 weeks of evenings.
Total cost: $0 (everything on free tiers).
Total sanity lost: Some. Worth it.

This article is the story of that journey — including all the mistakes, errors, and "why is this not working?!" moments. Because those are the parts tutorials skip.

Let's begin.

What I Deployed: A Brief Intro to the App
Before we dive into DevOps, let me quickly introduce the app

BloodLinker is a blood donation management system with three user roles:

  • 🩸 Donors — Register, receive notifications about matching blood requests in their district, respond to requests, track donation history with a 90-day medical eligibility countdown.
  • 🏥 Hospitals — Register (pending admin approval), post blood requests, view donor responses, mark donations as completed.
  • 🛡️ Admins — Approve hospital registrations, monitor platform activity, manage users.

The stack:

  • Frontend: React 18 + Vite + Tailwind CSS
  • Backend: Node.js + Express + JWT authentication
  • Database: MongoDB Atlas (cloud-hosted, free tier)
  • DevOps: Docker, Docker Compose, Nginx, Jenkins, Let's Encrypt
  • Cloud: AWS EC2 (Ubuntu 24.04, t3.micro)

Live demo: https://bloodlinker.is-a.dev

important - The DevOps principles are 100% universal. If you have any Node.js + React app (or even just a Node.js API), you can apply everything here.

The DevOps Stack — Every Tool Explained for Beginners
Before we touch any configuration, let me explain what each tool does and why it matters. Bookmark this section

AWS EC2

Instead of hosting on your laptop (which turns off when you close the lid), you rent a computer that runs 24/7 in an Amazon data center somewhere in the world.

You pay monthly, live there with EC2 — you rent compute power by the hour.

Cost: Free tier gives you 750 hours/month of t3.micro for 12 months. That's enough for one instance running 24/7 for free.

Docker

A tool that packages your app with everything it needs to run (code, runtime, dependencies, system libraries, config) into a single unit called a "container."

Why we use it: Solves the "it works on my machine" problem. The container runs the exact same way on your laptop, my server, or any cloud.

Analogy:

  • Without Docker: You give someone a recipe + raw ingredients + your oven temperature. They might get a different result.
  • With Docker: You give them a fully cooked meal in a sealed box. Everyone gets the same result.

Jenkins — The CI/CD Server

What it is: An automation server that runs your deployment pipeline.

Why we use it: Every time you git push, Jenkins automatically:

  1. Pulls the new code
  2. Builds new Docker images
  3. Stops old containers
  4. Starts new containers
  5. Verifies the deployment worked

Nginx — The Reverse Proxy

What it is: A web server that receives requests on ports 80/443 and routes them to the correct backend container based on the URL path.

Why we use it: Users shouldn't have to remember port numbers. https://bloodlinker.is-a.dev is way better than IP Address.

Let's Encrypt + Certbot — Free SSL Certificates

What it is: Let's Encrypt is a non-profit certificate authority that gives out free SSL certificates. Certbot is a tool that automates getting and renewing them.

Why we use it: HTTPS is non-negotiable in 2026. Without it:

  • Browsers show "Not Secure" warnings
  • Google penalizes your SEO
  • Users don't trust the site

GitHub Webhooks — The Automation Trigger

What it is: GitHub sends a small HTTP request to your server whenever you push code.

Why we use it: So Jenkins knows when to run the pipeline automatically. Without it, you'd have to click "Build Now" every time.

*Let's start the deployment part
*

Phase 1: Setting Up AWS EC2

The first thing we need is a server. A real one, running in the cloud, with a public IP address that anyone on the internet can reach.

Let's rent one.

Step 1.1: Launch the Instance

Sign in to the AWS Console (https://console.aws.amazon.com) and navigate to EC2.

Click "Launch instance" and fill in:

Field Value
Name your_app_name-server
AMI Ubuntu Server 24.04 LTS (Free tier eligible)
Instance type t3.micro (Free tier eligible)
Key pair Create new → app_name-key → .pem format
Storage 20 GB gp3

⚠️ Important note about the key pair: When you click "Create key pair," a .pem file downloads. Move it to a safe folder immediately and never share it. You cannot download it again. If you lose it, you'll have to create a new key pair and reattach it — which is a pain.

Step 1.2: Configure Security Groups

This is where most beginners get stuck. A security group is a firewall that decides what traffic is allowed in.

Create a new security group named your_project_name-sg with these inbound rules:

Type Port Source Purpose
SSH 22 My IP Remote access
HTTP 80 Anywhere (0.0.0.0/0) Web traffic
HTTPS 443 Anywhere (0.0.0.0/0) Secure web traffic
Custom TCP 8080 My IP Jenkins UI

Why "My IP" for SSH? Only your computer can access the server via SSH. This is a security best practice. Every time your home IP changes (which happens with most ISPs), you'll need to update this rule. Trust me — you'll forget and get "Connection timed out" errors for 20 minutes before realizing.

Why "Anywhere" for HTTP/HTTPS? Because your users will access the site from anywhere. That's the point.

Step 1.3: Launch and Get Your Elastic IP

Click "Launch instance".

Wait 2 minutes, then allocate an Elastic IP:

  1. Left sidebar → Elastic IPs
  2. Click "Allocate Elastic IP address"
  3. Click "Allocate"
  4. Select the new IP → Actions → "Associate Elastic IP address"
  5. Attach to your bloodlinker-server instance

Why Elastic IP? EC2's default public IP changes every time you stop and start the instance. An Elastic IP is permanent (and free while attached to a running instance).

Save your Elastic IP somewhere safe. You'll use it dozens of times.

Step 1.4: SSH In for the First Time

Open PowerShell (Windows) or Terminal (Mac/Linux) and connect:

ssh -i "C:\Users\YourName\path\to\bloodlinker-key.pem" ubuntu@<YOUR_ELASTIC_IP> 

Common first-time errors:

  • "UNPROTECTED PRIVATE KEY FILE" — Windows gives .pem files too many permissions. Fix:
 icacls "path\to\bloodlinker-key.pem" /inheritance:r icacls "path\to\bloodlinker-key.pem" /grant:r "$($env:USERNAME):(R)" 
  • "Connection timed out" — Your security group doesn't allow SSH from your current IP. Update the rule to "My IP."
  • "Permission denied (publickey)" — Wrong key path, or the key isn't attached to this instance.

If it works, you'll see:

Welcome to Ubuntu 24.04.x LTS ... ubuntu@ip-172-31-xx-xxx:~$ 

You're inside a real cloud server. 🎉

Step 1.5: Basic Server Setup

Once connected, run:

# Update packages sudo apt update && sudo apt upgrade -y # Install essentials sudo apt install -y curl git unzip wget nano ufw # Set timezone (optional — replace with yours) sudo timedatectl set-timezone Asia/Colombo 

Add 2 GB of swap space (critical for t3.micro with only 1 GB RAM):

sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab 

Why swap? Your server has 1 GB of RAM. Docker + Jenkins + your app will exceed that. Swap = using disk space as overflow RAM. Slower, but prevents crashes.

Verify:

free -h 

You should see Swap: 2.0Gi.

Phase 2: Dockerizing the App

Now for the fun part. We're going to package the app into containers.

Step 2.1: Create the Backend Dockerfile

In your project's backend/ folder, create a file called Dockerfile (no extension):

# ============================================================ # Backend Dockerfile — Node.js + Express # ============================================================ FROM node:20-alpine # Set working directory WORKDIR /app # Copy package files first (better caching) COPY package*.json ./ # Install production dependencies only RUN npm install --omit=dev # Copy the rest of the code COPY . . # Expose port EXPOSE 5000 # Start the server CMD ["npm", "start"] 

Why node:20-alpine? Alpine Linux is a tiny distro (~5 MB vs ~900 MB for full Node images). Smaller image = faster deploy.

Why copy package*.json separately? Docker caching. If only your source code changes (not dependencies), Docker skips the npm install step. This saves 30-60 seconds per build.

Step 2.2: Create the Frontend Dockerfile

For the frontend, we use a multi-stage build. This is a professional technique that keeps the final image tiny.

Create frontend/Dockerfile:

# ============================================================ # Frontend Dockerfile — Multi-stage build # ============================================================ # ---------- STAGE 1: BUILD ---------- FROM node:20-alpine AS builder WORKDIR /app # Install dependencies COPY package*.json ./ RUN npm install # Copy source code COPY . . # Accept the API URL as a build argument ARG VITE_API_URL ENV VITE_API_URL=$VITE_API_URL # Build the React app RUN npm run build # ---------- STAGE 2: SERVE ---------- FROM nginx:alpine # Remove default Nginx welcome page RUN rm -rf /usr/share/nginx/html/* # Copy the built React app COPY --from=builder /app/dist /usr/share/nginx/html # Copy custom Nginx config for SPA routing COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] 

Why multi-stage?

  • Stage 1 has Node.js + build tools (~300 MB of temporary stuff)
  • Stage 2 only copies the built dist/ folder (~25 MB final image)

Result: A production image that's 10x smaller than a naive single-stage build.

⚠️ The critical bit: VITE_API_URL is passed as a build argument. Vite bakes it into the JS bundle at build time. This means if the URL is wrong, you must rebuild — restarting won't help. Remember this. It will save you hours later. (Spoiler: it doesn't save me hours later.)

Step 2.3: Create Frontend's Nginx Config

Create frontend/nginx.conf:

# ============================================================ # Nginx config INSIDE the frontend container # ============================================================ server { listen 80; root /usr/share/nginx/html; index index.html; # SPA routing — send everything to index.html location / { try_files $uri $uri/ /index.html; } # Cache static assets location ~* \.(js|css|png|jpg|svg|woff2?)$ { expires 1y; add_header Cache-Control "public, immutable"; } gzip on; gzip_types text/css application/javascript application/json; } 

Why this matters: Without try_files $uri $uri/ /index.html, refreshing /dashboard gives you a 404 — because there's no file at that path. This config sends all unknown routes to index.html, letting React Router handle navigation.

Step 2.4: Create .dockerignore Files

Never copy node_modules or .env into Docker images. Create backend/.dockerignore and frontend/.dockerignore:

node_modules .env .env.local .git .gitignore *.md .vscode npm-debug.log 

Why: node_modules is huge and gets reinstalled inside the container. .env contains secrets that should never be baked into images.

Step 2.5: Create docker-compose.yml

This single file orchestrates both containers. Create it at the project root:

services: backend: build: context: ./backend dockerfile: Dockerfile container_name: bloodlinker-backend restart: unless-stopped environment: - MONGO_URI=${MONGO_URI} - JWT_SECRET=${JWT_SECRET} - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d} - NODE_ENV=production - PORT=5000 ports: - "5000:5000" networks: - bloodlinker-network healthcheck: test: ["CMD", "wget", "--spider", "http://localhost:5000/api/health"] interval: 30s timeout: 5s retries: 3 frontend: build: context: ./frontend dockerfile: Dockerfile args: VITE_API_URL: ${VITE_API_URL} container_name: bloodlinker-frontend restart: unless-stopped ports: - "3000:80" networks: - bloodlinker-network depends_on: - backend networks: bloodlinker-network: driver: bridge 

Key concepts:

  • ${MONGO_URI} — Read from .env at the project root (git-ignored)
  • restart: unless-stopped — Containers auto-start on server reboot
  • healthcheck — Docker monitors the backend's health
  • networks — Containers can reach each other by name

Step 2.6: Create .env at Project Root

Create .env at the project root (and add it to .gitignore):

MONGO_URI=mongodb+srv://user:pass@cluster.mongodb.net/bloodlife?retryWrites=true&w=majority JWT_SECRET=your_super_secret_key_here JWT_EXPIRES_IN=7d VITE_API_URL=https://bloodlinker.is-a.dev/api NODE_ENV=production 

⚠️ Add .env to .gitignore right now. If you accidentally commit this, your MongoDB password leaks to GitHub forever. Rotate the password if that happens.

Step 2.7: Test Locally

Before deploying to AWS, test locally:

docker compose up -d --build 

Then open http://localhost:3000 in your browser. If it loads, you're ready to move to the server.

Common first-time errors:

  • "Cannot find module" — Your Dockerfile didn't copy a file. Check COPY . .
  • "MongoDB connection failed" — Wrong MONGO_URI or MongoDB Atlas IP whitelist missing 0.0.0.0/0
  • "Port already in use" — Something else is using port 5000 or 3000. Kill it: docker stop $(docker ps -aq)

Once it works locally, stop the containers:

docker compose down 

Phase 3: Installing Jenkins on the Server

Now let's install the automation server. SSH into your EC2 instance.

Step 3.1: Install Java

Jenkins requires Java 17 or higher:

sudo apt install -y openjdk-21-jre 

Verify:

java -version 

Step 3.2: Install Jenkins

Add the Jenkins repository:

# Add GPG key sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \ https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key # Add repository echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc]" \ https://pkg.jenkins.io/debian-stable binary/ | sudo tee \ /etc/apt/sources.list.d/jenkins.list > /dev/null # Install sudo apt update sudo apt install -y jenkins 

⚠️ Pro tip: If you see NO_PUBKEY errors, your GPG key is outdated. The correct URL ends in jenkins.io-2026.key, not 2023.key. This tripped me up for 30 minutes.

Step 3.3: Configure Jenkins Memory

Critical for t3.micro with 1 GB RAM. By default, Jenkins eats all your memory. Cap it:

sudo systemctl edit jenkins 

Add:

[Service] Environment="JAVA_OPTS=-Xms128m -Xmx400m -XX:MaxMetaspaceSize=192m" 

Reload and restart:

sudo systemctl daemon-reload sudo systemctl restart jenkins 

Why -Xmx400m? This caps Jenkins' heap at 400 MB. Without this, Jenkins + Docker + your app will exceed 1 GB RAM and the server will start swapping hard.

Step 3.4: Give Jenkins Access to Docker

Jenkins needs to run docker commands. Add the jenkins user to the docker group:

sudo usermod -aG docker jenkins sudo systemctl restart jenkins 

Verify:

sudo -u jenkins docker ps 

Should return an empty list, not "permission denied."

Step 3.5: Unlock Jenkins

Open http://<YOUR_ELASTIC_IP>:8080 in your browser.

Get the initial admin password:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword 

Paste it, click Continue, install suggested plugins, create an admin user, and finish.

Step 3.6: Install Required Plugins

Go to Manage Jenkins → Plugins → Available plugins. Install:

  • GitHub Integration Plugin — receives webhooks
  • Docker Pipeline — runs Docker commands in pipelines
  • Docker — Docker integration for Jenkins

Restart Jenkins after installing.

Step 3.7: Store Your Secrets

We need to give Jenkins access to the MongoDB URI and JWT secret — without ever putting them in Git.

Go to Manage Jenkins → Credentials → System → Global credentials → Add Credentials:

Credential 1:

  • Kind: Secret text
  • Secret: <your actual MongoDB URI>
  • ID: bloodlinker-mongo-uri (must match exactly)

Credential 2:

  • Kind: Secret text
  • Secret: <your actual JWT secret>
  • ID: bloodlinker-jwt-secret

Why "Secret text"? It's the simplest credential type. Jenkins encrypts it and never shows it in logs.

⚠️ The ID must be exact. If your Jenkinsfile references bloodlinker-mongo-uri, the credential must have that exact same ID (case-sensitive, no spaces).

Phase 4: The Jenkinsfile — Pipeline as Code

This is the heart of the CI/CD setup. The Jenkinsfile is a Groovy script that tells Jenkins what to do.

Create a file called Jenkinsfile at your project root (no extension):

// ============================================================ // BloodLinker CI/CD Pipeline // ============================================================ pipeline { agent any // Environment variables — secrets come from Jenkins credentials environment { MONGO_URI = credentials('bloodlinker-mongo-uri') JWT_SECRET = credentials('bloodlinker-jwt-secret') JWT_EXPIRES_IN = '7d' NODE_ENV = 'production' VITE_API_URL = 'https://bloodlinker.is-a.dev/api' } stages { stage('Checkout') { steps { echo '📥 Code checked out from GitHub' sh 'pwd && ls -la' } } stage('Prepare Environment') { steps { echo '🔐 Writing .env file from credentials' sh ''' cat > .env <<EOF MONGO_URI=${MONGO_URI} JWT_SECRET=${JWT_SECRET} JWT_EXPIRES_IN=${JWT_EXPIRES_IN} NODE_ENV=${NODE_ENV} VITE_API_URL=${VITE_API_URL} EOF echo '✅ .env created' ''' } } stage('Build Backend') { steps { echo '🔨 Building backend Docker image...' sh 'docker compose build backend' } } stage('Build Frontend') { steps { echo '🔨 Building frontend Docker image (no cache)...' sh 'docker compose build --no-cache frontend' } } stage('Deploy') { steps { echo '🚀 Deploying new containers...' sh 'docker compose down --remove-orphans || true' sh 'docker compose up -d --force-recreate' sh 'docker compose ps' } } stage('Health Check') { steps { echo '🏥 Checking backend health...' sh ''' sleep 15 curl -f http://localhost:5000/api/health || exit 1 ''' } } } post { success { echo '🎉 Deployment successful!' } failure { echo '❌ Deployment failed. Check logs above.' } always { sh 'rm -f .env || true' } } } 

Walkthrough of Each Stage

Checkout — Jenkins automatically clones your repo before the pipeline runs. This stage just verifies the files are there.

Prepare Environment — Writes a .env file from Jenkins credentials. This is why we never commit .env to Git — it's generated fresh on every build.

Build Backend — Runs docker compose build backend. Docker builds the image.

Build Frontend — Uses --no-cache. Why? This is critical. Because VITE_API_URL is a build-time argument, Docker caches might reuse an old image with a stale URL. --no-cache forces a fresh build.

Deploy — Stops old containers, starts new ones. --force-recreate ensures containers restart with the new images.

Health Check — Waits 15 seconds, then hits /api/health. If it fails, the pipeline fails — so we don't deploy broken builds.

Post actions — Cleans up .env from the workspace, reports success or failure.

Create the Jenkins Job

In the Jenkins UI:

  1. New Item → Pipeline → name it bloodlinker-pipeline
  2. Under Build Triggers, check GitHub hook trigger for GITScm polling (we'll configure the webhook later)
  3. Under Pipeline:
    • Definition: Pipeline script from SCM
    • SCM: Git
    • Repository URL: https://github.com/yourname/repo.git
    • Branch Specifier: */main
    • Script Path: Jenkinsfile
  4. Save

Trigger the First Build

Click "Build Now". Watch the Console Output.

If it succeeds → you've deployed your app via Jenkins! 🎉

**If it fails → this is where the learning begins.

Phase 5: Nginx Reverse Proxy + Custom Domain

Right now, your app runs on http://<YOUR_IP>:3000 (frontend) and http://<YOUR_IP>:5000/api (backend). That's ugly, and it exposes internal ports.

Let's fix that with Nginx and a real domain.

Step 5.1: Register a Free Custom Domain

For a portfolio project, you don't want to pay $15/year for a domain. Instead, use is-a.dev — a free subdomain service for developers.

Step 5.2: Install Nginx on the Server

SSH back into your EC2:

sudo apt install -y nginx sudo systemctl start nginx sudo systemctl enable nginx 

Test:

http://<YOUR_ELASTIC_IP> 

You'll see the Nginx welcome page. Delete it:

sudo rm /etc/nginx/sites-enabled/default 

Step 5.3: Configure Nginx as a Reverse Proxy

In SSH Create file - /etc/nginx/sites-available/bloodlinker:

sudo nano /etc/nginx/sites-available/bloodlinker 

the content of above file based on my project :

server { listen 80; server_name yourname.is-a.dev www.yourname.is-a.dev; client_max_body_size 20M; # API routes → backend container location /api/ { proxy_pass http://localhost:5000; 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_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # Everything else → frontend container location / { proxy_pass http://localhost: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; } } 

Enable it:
In your SSH

sudo ln -s /etc/nginx/sites-available/bloodlinker /etc/nginx/sites-enabled/ sudo nginx -t # ALWAYS test before reloading! sudo systemctl reload nginx 

Step 5.4: Test

Open http://yourname.is-a.dev. You should see your app! 🎉

Common issues:

  • "Connection refused" — Nginx isn't running, or the config isn't loaded. Check sudo systemctl status nginx.
  • "502 Bad Gateway" — Nginx is running, but your containers aren't. Run docker ps to check.

Phase 6: HTTPS with Let's Encrypt

HTTP is nice, but HTTPS is table stakes in 2026. Browsers flag HTTP sites as "Not Secure," Google penalizes them in rankings, and users don't trust them.

Let's add free HTTPS.

Step 6.1: Install Certbot

In your SSH

sudo apt update sudo apt install -y certbot python3-certbot-nginx 

Certbot is the official tool for Let's Encrypt. The python3-certbot-nginx plugin automatically modifies your Nginx config.

Step 6.2: Get the Certificate

In your SSH

sudo certbot --nginx -d yourname.is-a.dev -d www.yourname.is-a.dev 

Certbot asks a series of questions:

  1. Email address: Your real email (for renewal reminders)
  2. Agree to TOS: A
  3. Share email with EFF: N (or Y, your choice)
  4. Redirect HTTP to HTTPS: 2 (choose this!)

What happens next:

  • Certbot verifies you own the domain (via HTTP challenge)
  • Downloads a certificate from Let's Encrypt (valid 90 days)
  • Automatically updates your Nginx config to add HTTPS
  • Sets up HTTP → HTTPS redirect

Expected output:

Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/yourname.is-a.dev/fullchain.pem Key is saved at: /etc/letsencrypt/live/yourname.is-a.dev/privkey.pem This certificate expires on 2027-XX-XX. ... Congratulations! You have successfully enabled HTTPS 

Step 6.3: Verify

Open https://yourname.is-a.dev. You should see:

  • 🔒 Green padlock in the address bar
  • ✅ Click the lock → "Connection is secure"

Also test:

http://yourname.is-a.dev 

Should auto-redirect to HTTPS.

Step 6.4: Auto-Renewal

Certificates expire every 90 days. Certbot handles this automatically via a systemd timer. Test it:

In your SSH

sudo certbot renew --dry-run 

Should say: "Congratulations, all simulated renewals succeeded."

You never have to think about SSL certificates again. Certbot checks twice a day and renews when needed.

Phase 7: GitHub Webhooks — True Automation

Right now, you have to click "Build Now" in Jenkins every time you want to deploy. That's not CI/CD — that's manual.

GitHub webhooks fix this. Every git push triggers the pipeline automatically.

Step 7.1: Prepare the Security Group

GitHub's servers need to reach Jenkins on port 8080. But GitHub's IPs are dynamic, so we can't whitelist them individually.

Update your security group:

  1. AWS Console → EC2 → Security Groups → bloodlinker-sg
  2. Edit the Custom TCP 8080 rule
  3. Change source from "My IP" to "Anywhere (0.0.0.0/0)"

⚠️ Security trade-off: This exposes Jenkins publicly. Mitigate by:

  • Jenkins still requires authentication
  • Use strong credentials
  • (Later) Put Jenkins behind a subdomain with Nginx + SSL

Step 7.2: Enable Webhook Trigger in Jenkins

Jenkins → Your job → Configure → Build Triggers → check:

☑ GitHub hook trigger for GITScm polling

Save.

Step 7.3: Create the Webhook in GitHub

  1. GitHub repo → Settings → Webhooks → Add webhook
  2. Payload URL: http://<YOUR_ELASTIC_IP>:8080/github-webhook/
    • ⚠️ The trailing slash is critical
  3. Content type: application/json
  4. Which events? Just the push event
  5. Active: ☑
  6. Click Add webhook

GitHub sends a ping test event immediately. You should see a green ✅ next to your webhook.

Step 7.4: Test It

Make a tiny change to your app and push:

Source:https://dev.to/dishan_sanjula_e92a52f862/deploying-a-production-ready-mern-app-with-docker-jenkins-and-aws-a-beginners-journey-2p50

Related posts