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:
- Pulls the new code
- Builds new Docker images
- Stops old containers
- Starts new containers
- 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:
- Left sidebar → Elastic IPs
- Click "Allocate Elastic IP address"
- Click "Allocate"
- Select the new IP → Actions → "Associate Elastic IP address"
- Attach to your
bloodlinker-serverinstance
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
.pemfiles 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.envat 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
Dockerfiledidn't copy a file. CheckCOPY . . - "MongoDB connection failed" — Wrong
MONGO_URIor MongoDB Atlas IP whitelist missing0.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:
- New Item → Pipeline → name it
bloodlinker-pipeline - Under Build Triggers, check GitHub hook trigger for GITScm polling (we'll configure the webhook later)
- Under Pipeline:
- Definition: Pipeline script from SCM
- SCM: Git
- Repository URL:
https://github.com/yourname/repo.git - Branch Specifier:
*/main - Script Path:
Jenkinsfile
- 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 psto 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:
- Email address: Your real email (for renewal reminders)
- Agree to TOS:
A - Share email with EFF:
N(orY, your choice) - 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:
- AWS Console → EC2 → Security Groups →
bloodlinker-sg - Edit the Custom TCP 8080 rule
- 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
- GitHub repo → Settings → Webhooks → Add webhook
- Payload URL:
http://<YOUR_ELASTIC_IP>:8080/github-webhook/- ⚠️ The trailing slash is critical
- Content type:
application/json - Which events?
Just the push event - Active: ☑
- 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