<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[DEVOPSNOTE]]></title><description><![CDATA[DevNotes is a practical DevOps and Site Reliability Engineering (SRE) blog focused on real-world tutorials, automation, cloud infrastructure, CI/CD pipelines, Linux, Docker, Kubernetes, Ansible, NGINX, and system design. Learn DevOps tools, best practices, deployment strategies, monitoring, and reliability engineering through simple, step-by-step guides designed for beginners and professionals.]]></description><link>https://blog.sachindu.me</link><image><url>https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/logos/685b63b17e7d46cc40b0aa24/ab822c7b-d90a-463f-8b12-7822f71b4248.png</url><title>DEVOPSNOTE</title><link>https://blog.sachindu.me</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 03:40:55 GMT</lastBuildDate><atom:link href="https://blog.sachindu.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Self-Healing GitOps Based Micro-services Platform on GKE with Argo CD, HPA & n8n]]></title><description><![CDATA[Repository Link: https://github.com/sachindumalshan/gitops-repo.git
Architecture Overview
Here is the system level architecture.
Google Cloud (GKE)
│
├── Kubernetes Cluster (gitops-cluster)
│   ├── de]]></description><link>https://blog.sachindu.me/self-healing-gitops-gke-argocd-n8n</link><guid isPermaLink="true">https://blog.sachindu.me/self-healing-gitops-gke-argocd-n8n</guid><category><![CDATA[GCP]]></category><category><![CDATA[GCP DevOps]]></category><category><![CDATA[Devops]]></category><category><![CDATA[n8n]]></category><category><![CDATA[GKE cluster]]></category><category><![CDATA[gke]]></category><category><![CDATA[ArgoCD]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[automation]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Docker]]></category><category><![CDATA[docker images]]></category><category><![CDATA[gitops]]></category><category><![CDATA[HPA]]></category><category><![CDATA[Microservices]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Sat, 21 Feb 2026 15:57:12 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/64494b80-bb24-4a65-902c-3859e6a097ca.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p><strong>Repository Link:</strong> <a href="https://github.com/sachindumalshan/gitops-repo.git">https://github.com/sachindumalshan/gitops-repo.git</a></p>
<h2>Architecture Overview</h2>
<p>Here is the system level architecture.</p>
<pre><code class="language-plaintext">Google Cloud (GKE)
│
├── Kubernetes Cluster (gitops-cluster)
│   ├── default namespace
│   │   ├──── service-a (Device Service)
│   │   │     ├─ app.py
│   │   │     ├─ deployment.yaml
│   │   │     ├─ hpa.yaml
│   │   │     ├─ Dockerfile
│   │   │     └─ service.yaml
│   │   ├──── service-b (Sensor Service)
│   │   │     ├─ app.py
│   │   │     ├─ deployment.yaml
│   │   │     ├─ hpa.yaml
│   │   │     ├─ Dockerfile
│   │   │     └─ service.yaml
│   │   └──── service-c (Alert Service)
│   │         ├─ app.py
│   │         ├─ deployment.yaml
│   │         ├─ hpa.yaml
│   │         ├─ Dockerfile
│   │         └─ service.yaml
│   │
│   ├── argocd namespace
│   │   ├── argocd-server
│   │   ├── argocd-repo-server
│   │   ├── argocd-application-controller
│   │   └── argocd-dex-server
│   │
│   └── automation namespace
│       └── n8n
│
├── Google Artifact Registry
│
└── GitHub (Source of Truth)
</code></pre>
<h2>End-to-End Flow</h2>
<pre><code class="language-plaintext">Developer pushes code → GitHub
        ↓
Argo CD detects change
        ↓
Argo CD syncs to GKE
        ↓
Pods deployed / updated
        ↓
Kubernetes handles:
    - Self-healing
    - Auto-scaling (HPA)
        ↓
n8n monitors services
        ↓
Slack alerts if failure
</code></pre>
<hr />
<h2>PHASE 1 - Install gcloud CLI &amp; Setup GKE</h2>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/309f095f-8de7-48da-bc44-66ea2015ec75.png" alt="" style="display:block;margin:0 auto" />

<p>Update System</p>
<pre><code class="language-bash">sudo apt-get update
</code></pre>
<p>Install required packages:</p>
<pre><code class="language-bash">sudo apt-get install apt-transport-https ca-certificates gnupg curl
</code></pre>
<p>Import Google Cloud Public Key</p>
<ul>
<li>For newer distributions:</li>
</ul>
<pre><code class="language-bash">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
</code></pre>
<ul>
<li>For older distributions:</li>
</ul>
<pre><code class="language-bash">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
</code></pre>
<ul>
<li>If unsupported:</li>
</ul>
<pre><code class="language-bash">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
</code></pre>
<p>Add Repository</p>
<ul>
<li>Newer systems:</li>
</ul>
<pre><code class="language-bash">echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
</code></pre>
<ul>
<li>Older systems:</li>
</ul>
<pre><code class="language-bash">echo "deb https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
</code></pre>
<p>Install gcloud</p>
<pre><code class="language-bash">sudo apt-get update &amp;&amp; sudo apt-get install google-cloud-cli
</code></pre>
<p>Login &amp; Configure Project</p>
<pre><code class="language-bash">gcloud auth login
gcloud projects list
gcloud config set project gitops-self-healing-7687
</code></pre>
<p>Enable APIs:</p>
<pre><code class="language-bash">gcloud services enable \
  container.googleapis.com \
  compute.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com \
</code></pre>
<p>🟢 Create GKE Cluster</p>
<pre><code class="language-bash">gcloud container clusters create gitops-cluster \
  --zone asia-south1-a \
  --num-nodes 2 \
  --machine-type e2-medium
</code></pre>
<p>Get credentials:</p>
<pre><code class="language-bash">gcloud container clusters get-credentials gitops-cluster \
  --zone asia-south1-a
</code></pre>
<h3><mark class="bg-yellow-200 dark:bg-yellow-500/30">❗ ERROR 1 — GKE Auth Plugin Missing</mark></h3>
<p>If you see authentication errors:</p>
<p>Install plugin:</p>
<pre><code class="language-bash">sudo apt-get install google-cloud-cli-gke-gcloud-auth-plugin
</code></pre>
<p>Verify:</p>
<pre><code class="language-bash">which gke-gcloud-auth-plugin
</code></pre>
<p>Refresh kubeconfig:</p>
<pre><code class="language-bash">gcloud container clusters get-credentials gitops-cluster \
  --zone asia-south1-a
</code></pre>
<p>Test:</p>
<pre><code class="language-bash">kubectl get nodes
</code></pre>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/e87eaba0-dc0d-476e-935a-fcb980c01005.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>PHASE 2 - Build &amp; Push Python Microservice</h2>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/fba498cd-b5bf-4121-b70d-2b6ff7b77830.png" alt="" style="display:block;margin:0 auto" />

<p>Sample Flask App: app.py</p>
<pre><code class="language-python">from flask import Flask
import os

app = Flask(__name__)

@app.route("/health")
def health():
    if os.getenv("FAIL") == "true":
        return "FAIL", 500
    return "OK", 200

@app.route("/")
def home():
    return "Service A Running", 200

app.run(host="0.0.0.0", port=8080)
</code></pre>
<p>Dockerfile</p>
<pre><code class="language-dockerfile">FROM python:3.11-slim
WORKDIR /app
RUN pip install flask
COPY app.py .
CMD ["python", "app.py"]
</code></pre>
<p>Create Artifact Registry</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/74835e73-7b6b-41ca-a22b-81c38ed63557.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-bash">gcloud artifacts repositories create docker-repo \
  --repository-format=docker \
  --location=asia-south1
</code></pre>
<p>Configure Docker:</p>
<pre><code class="language-bash">gcloud auth configure-docker asia-south1-docker.pkg.dev
</code></pre>
<p>Build &amp; push:</p>
<pre><code class="language-bash">docker build -t asia-south1-docker.pkg.dev/gitops-self-healing/docker-repo/service-a:v1 .
docker push asia-south1-docker.pkg.dev/gitops-self-healing/docker-repo/service-a:v1
</code></pre>
<h3><mark class="bg-yellow-200 dark:bg-yellow-500/30">❗ ERROR 2 — ImagePullBackOff</mark></h3>
<p>Cause: GKE nodes don’t have permission to pull images.</p>
<p>Get project number:</p>
<pre><code class="language-bash">gcloud projects describe gitops-self-healing \
  --format="value(projectNumber)"
</code></pre>
<p>Grant permission:</p>
<pre><code class="language-bash">gcloud projects add-iam-policy-binding gitops-self-healing \
  --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
  --role="roles/artifactregistry.reader"
</code></pre>
<p>Restart pod:</p>
<pre><code class="language-bash">kubectl delete pod &lt;pods_name&gt; #service-a-595cc8c965-tlmrh
kubectl get pods -w
</code></pre>
<hr />
<h2>PHASE 3 - Kubernetes Self-Healing</h2>
<p>Deployment with probes:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
spec:
  replicas: 1
  selector:
    matchLabels:
      app: service-a
  template:
    metadata:
      labels:
        app: service-a
    spec:
      containers:
      - name: service-a
        image: asia-south1-docker.pkg.dev/gitops-self-healing/docker-repo/service-a:v1
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
</code></pre>
<p>Apply:</p>
<pre><code class="language-bash">kubectl apply -f deployment.yaml
</code></pre>
<p>Test:</p>
<pre><code class="language-bash">kubectl set env deployment/service-a FAIL=true
</code></pre>
<p>🔥 Kubernetes automatically restarts unhealthy pods.</p>
<hr />
<h2>PHASE 4 - Auto Scaling (HPA)</h2>
<p>Check metrics: (CPU/Memory)</p>
<pre><code class="language-bash">kubectl top pods
</code></pre>
<p>HPA File:</p>
<pre><code class="language-yaml">apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: service-a-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: service-a
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
</code></pre>
<p>Apply HPA:</p>
<pre><code class="language-bash">kubectl apply -f hpa.yaml
kubectl get hpa -w
</code></pre>
<p>HPA scales based on CPU metrics from Metrics Server.</p>
<hr />
<h2>PHASE 5 - Push to GitHub</h2>
<p>Upload files to the GitHub Repository</p>
<pre><code class="language-bash">git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/&lt;your-username&gt;/gitops-repo.git
git push -u origin main
</code></pre>
<hr />
<h2>PHASE 6 - Install Argo CD (GitOps Engine)</h2>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/9b2dabe6-ca1c-4b55-808e-b86abd811d13.png" alt="" style="display:block;margin:0 auto" />

<p>Create namespace:</p>
<pre><code class="language-bash">kubectl create namespace argocd
</code></pre>
<p>Install:</p>
<pre><code class="language-bash">kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
</code></pre>
<p>Verify:</p>
<pre><code class="language-bash">kubectl get pods -n argocd
</code></pre>
<p>How you access Argo CD UI:</p>
<p><strong>Option 1 (Learning / Local / GKE lab)</strong></p>
<pre><code class="language-yaml"># Port-forward
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Access
https://localhost:8080
</code></pre>
<p><strong>Option 2 — Change Service Type to LoadBalancer (Direct Public IP)</strong></p>
<p>Instead of port-forward, expose Argo CD externally.</p>
<pre><code class="language-yaml"># Check service:
kubectl get svc argocd-server -n argocd

#It is probably:
ClusterIP

# Patch it:
kubectl patch svc argocd-server -n argocd \ -p '{"spec": {"type": "LoadBalancer"}}'

# Now check:
kubectl get svc argocd-server -n argocd

# Access:
https://&lt;external-ip&gt;
</code></pre>
<p><strong>Option 3 — Use Ingress (Recommended for Real DevOps Setup)</strong></p>
<p>Instead of exposing service directly, use an Ingress.</p>
<p>This allows:</p>
<ul>
<li><p>Domain name</p>
</li>
<li><p>HTTPS with TLS</p>
</li>
<li><p>Multiple apps behind one Load Balancer</p>
</li>
</ul>
<p>In GKE, Ingress uses: Google Cloud Load Balancing</p>
<p>Example Ingress:</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: argocd-ingress namespace: argocdspec: rules: - host: argocd.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: argocd-server port: number: 443
</code></pre>
<p><em><strong>Get Admin Password</strong></em></p>
<pre><code class="language-bash">kubectl get secret argocd-initial-admin-secret \
  -n argocd \
  -o jsonpath="{.data.password}" | base64 -d
</code></pre>
<p>Login with:</p>
<ul>
<li><p>Username: <code>admin</code></p>
</li>
<li><p>Password: <em>decoded value</em></p>
</li>
</ul>
<h3>GitOps Application Creation</h3>
<ul>
<li><p>Sync Policy: Automatic</p>
</li>
<li><p>Auto-Prune: Enabled</p>
</li>
<li><p>Self-Heal: Enabled</p>
</li>
</ul>
<p>Argo CD now continuously reconciles cluster state with Git.</p>
<pre><code class="language-yaml"># STEP 1️⃣ What you see after login (IMPORTANT)
You’ll see:
- Empty dashboard
- No applications yet

## STEP 2️⃣ Create your FIRST GitOps Application

Click: + NEW APP

Fill like this:
- Application Name: `service-a`
- Project: `default`
- Sync Policy: `Automatic`
    - ✅ Automatic
    - ✅ Auto-Prune        
    - ✅ Self-Heal        
- Repository URL:
  https://github.com/&lt;your-username&gt;/gitops-repo
- Revision:
  `main`
- Path:
  `service-a`

📌 This path must contain:
- `deployment.yaml`    
- `service.yaml`    
- `hpa.yaml`
    
### Destination
- Cluster URL:
  `https://kubernetes.default.svc`
- Namespace:
  `default`

Click: CREATE

## STEP 3️⃣ What happens immediately after clicking CREATE

Behind the scenes:
1.  Argo CD pulls Git repo
2.  Reads YAML files    
3.  Compares with live cluster    
4.  Applies manifests   
5.  Shows app as **Healthy / Synced**
    
You’ll see:
- Green boxes
- Pod creation in real time

## STEP 4️⃣ Verify from terminal (important habit)
kubectl get pods
kubectl get svc
kubectl get hpa

## STEP 5️⃣ Prove SELF-HEALING

# Break something manually
kubectl delete pod -l app=service-a

Result:
- Pod deleted
- Deployment recreates pod
- Argo CD remains synced

# Create configuration drift
kubectl scale deployment service-a --replicas=5

Watch Argo CD UI: It will revert replicas back to Git value automatically.
</code></pre>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/3cddc484-dbec-4223-9394-f7285fb4a743.png" alt="" style="display:block;margin:0 auto" />

<h3><mark class="bg-yellow-200 dark:bg-yellow-500/30">❗ ERROR 3 — Application Not Syncing</mark></h3>
<p>Common causes:</p>
<ul>
<li><p>Wrong repo path</p>
</li>
<li><p>Wrong branch</p>
</li>
<li><p>Missing YAML files</p>
</li>
<li><p>Incorrect namespace</p>
</li>
</ul>
<p>Fix path and resync.</p>
<hr />
<h2>PHASE 7 - Expose Service via LoadBalancer</h2>
<p>Update Service YAML:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: service-a
  namespace: default
spec:
  selector:
    app: service-a   # matches your pod labels
  ports:
    - protocol: TCP
      port: 80       # the port clients use to access
      targetPort: 5000  # the port your container listens on
  type: LoadBalancer   # gives an external IP in GKE
</code></pre>
<p>Commit &amp; push:</p>
<pre><code class="language-bash">git add service-a/
git commit -m "Change python app port to 5000"
git push origin main
</code></pre>
<p>Check external IP:</p>
<pre><code class="language-bash">kubectl get svc service-a

# Access
http://&lt;EXTERNAL-IP&gt;
</code></pre>
<hr />
<h2>PHASE 8 - Multi-Service IoT Micro-services</h2>
<p>Implement three services to understand how they communicate internally and add those to the ArgoCD.</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p><strong>service-a</strong></p></th><th><p>Device Service</p></th></tr><tr><td><p><strong>service-b</strong></p></td><td><p><strong>Sensor Service</strong></p></td></tr><tr><td><p><strong>service-c</strong></p></td><td><p><strong>Alert Service</strong></p></td></tr></tbody></table>

<p>As stated earlier created application, create 3 applications for 3 services like in below image.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/de6622c8-832b-462a-a82f-6040ac3ec2dd.png" alt="" style="display:block;margin:0 auto" />

<h2>PHASE 9 - Setup n8n</h2>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/d0f17c98-62aa-44cf-a4fa-11b380eb2779.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Why Add n8n?</strong></p>
<p>Kubernetes + Argo CD already handle 80% of recovery.</p>
<p>But they <strong>do not</strong>:</p>
<ul>
<li><p>Send Slack alerts</p>
</li>
<li><p>Trigger email notifications</p>
</li>
<li><p>Execute external Git rollback</p>
</li>
<li><p>Call external APIs</p>
</li>
<li><p>Run conditional business logic</p>
</li>
</ul>
<p>That’s where <strong>n8n</strong> comes in.</p>
<h3>Why Install n8n Inside Kubernetes?</h3>
<p>You could run n8n: <strong>Inside Kubernetes</strong></p>
<p>We choose Kubernetes because:</p>
<ul>
<li><p>Same cluster access</p>
</li>
<li><p>Easy internal DNS communication</p>
</li>
<li><p>Scalable</p>
</li>
<li><p>Production-style deployment</p>
</li>
<li><p>Strong DevOps portfolio value</p>
</li>
</ul>
<h3>Step-by-Step — Install n8n in Kubernetes</h3>
<p>1️⃣ Create Namespace</p>
<pre><code class="language-bash">kubectl create namespace automation
</code></pre>
<p>Verify:</p>
<pre><code class="language-bash">kubectl get ns
</code></pre>
<p>2️⃣ Create n8n Deployment</p>
<p>Create file: <code>n8n-deployment.yaml</code></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: n8n
  namespace: automation
spec:
  replicas: 1
  selector:
    matchLabels:
      app: n8n
  template:
    metadata:
      labels:
        app: n8n
    spec:
      containers:
      - name: n8n
        image: n8nio/n8n:latest
        ports:
        - containerPort: 5678
        env:
        - name: N8N_BASIC_AUTH_ACTIVE
          value: "true"
        - name: N8N_BASIC_AUTH_USER
          value: "admin"
        - name: N8N_BASIC_AUTH_PASSWORD
          value: "admin123"
        - name: N8N_SECURE_COOKIE
          value: "false"
        # ── Add these four lines ──────────────────────────
        - name: N8N_EDITOR_BASE_URL
          value: "http://35.210.234.209"
        - name: WEBHOOK_URL
          value: "http://35.210.234.209/"
        - name: N8N_HOST
          value: "35.210.234.209"
        - name: N8N_PROTOCOL
          value: "http"
</code></pre>
<p>Apply:</p>
<pre><code class="language-bash">kubectl apply -f n8n-deployment.yaml
</code></pre>
<p>3️⃣ Create Service (NodePort)</p>
<p>Create file: <code>n8n-service.yaml</code></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: n8n
  namespace: automation
spec:
  type: NodePort
  selector:
    app: n8n
  ports:
  - port: 5678
    targetPort: 5678
</code></pre>
<p>Apply:</p>
<pre><code class="language-bash">kubectl apply -f n8n-service.yaml
</code></pre>
<p>Verify:</p>
<pre><code class="language-bash">kubectl get pods -n automation
kubectl get svc -n automation
</code></pre>
<p>Access n8n:</p>
<pre><code class="language-text">http://&lt;node-ip&gt;:&lt;nodeport&gt;
</code></pre>
<h3><mark class="bg-yellow-200 dark:bg-yellow-500/30">❗ ERROR — n8n Not Accessible</mark></h3>
<h3>Possible causes:</h3>
<ul>
<li><p>Firewall blocking NodePort</p>
</li>
<li><p>Wrong external IP</p>
</li>
<li><p>Pod not running</p>
</li>
<li><p>Service type incorrect</p>
</li>
</ul>
<p>Check:</p>
<pre><code class="language-bash">kubectl describe pod -n automation
kubectl describe svc n8n -n automation
</code></pre>
<h3>🟢 Build n8n Workflow — Service Health Monitor</h3>
<pre><code class="language-text">[Schedule Trigger] every 1 min
        ↓
[HTTP Request] service-a /live
        ↓
[HTTP Request] service-b /live
        ↓
[HTTP Request] service-c /live
        ↓
[Code Node] evaluate results
        ↓
[IF Node] has Issues?
       ↓           ↓
   [Slack]       [End]
</code></pre>
<p><strong>Node 1 - Schedule Trigger</strong></p>
<ul>
<li><p>Trigger Interval: Minutes</p>
</li>
<li><p>Every: 1</p>
</li>
</ul>
<p>This runs monitoring every 60 seconds.</p>
<p><strong>Nodes 2, 3, 4 — HTTP Requests</strong></p>
<p>Each node:</p>
<p>Method: GET</p>
<p>URLs:</p>
<pre><code class="language-plaintext">http://service-a.automation.svc.cluster.local/live
http://service-b.automation.svc.cluster.local/live
http://service-c.automation.svc.cluster.local/live
</code></pre>
<p>Name them exactly:</p>
<ul>
<li><p>Check Service A</p>
</li>
<li><p>Check Service B</p>
</li>
<li><p>Check Service C</p>
</li>
</ul>
<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">⚠️ IMPORTANT:</mark></p>
<p>Go to <strong>Settings tab → Enable “Continue on Fail”</strong></p>
<p>Without this, workflow stops on first failure.</p>
<p><strong>Node 5 — Code Node</strong></p>
<pre><code class="language-javascript">const serviceA = $('Check Service A').first();
const serviceB = $('Check Service B').first();
const serviceC = $('Check Service C').first();

const results = [
  { name: 'service-a', data: serviceA },
  { name: 'service-b', data: serviceB },
  { name: 'service-c', data: serviceC },
];

const issues = [];

for (const svc of results) {
  if (svc.data.error !== undefined) {
    issues.push({
      service: svc.name,
      detail: svc.data.error?.message || 'No response'
    });
  }
}

const lines = issues.map(i =&gt; `*\({i.service}* — DOWN\nDetail: \){i.detail}`).join('\n\n');

return [{
  json: {
    hasIssues: issues.length &gt; 0,
    message: issues.length &gt; 0 ? `🚨 Service Health Alert\n\n${lines}` : 'All OK'
  }
}];
</code></pre>
<p>⚠️ Node names inside <code>$('...')</code> must match EXACTLY.</p>
<p><strong>Node 6 — IF Node</strong></p>
<p>Condition:</p>
<pre><code class="language-plaintext">{{ $json.hasIssues }}
</code></pre>
<p>Operation: <code>is true</code></p>
<p>True → Slack<br />False → End</p>
<p><strong>Node 7 — Setup a Slack Bot</strong></p>
<pre><code class="language-yaml">1. Go to: https://api.slack.com/apps
2. Create New App: → From scratch
3. OAuth &amp; Permissions(Add scopes):
   * chat:write
   * chat:write.public
   * channels:read
4. Install to workspace:
5. Copy Bot Token (starts with xoxb-):

In n8n:
Settings → Credentials → Slack API → paste token

In Slack:
/invite @n8n-alerts

Slack Node Configuration:
* Resource: Message
* Operation: Send
* Channel: #alerts
* Message: {{ $json.message }}

Activate workflow.
</code></pre>
<p><strong>Testing the Workflow</strong></p>
<pre><code class="language-yaml">Change:
http://service-a/live

To:
http://service-x/live
</code></pre>
<p>Execute workflow.</p>
<p>✅ Slack alert should fire. Revert back.</p>
<hr />
<h2>PHASE 10 - Realistic Production Architecture</h2>
<p>Now your architecture becomes:</p>
<pre><code class="language-text">Kubernetes
 ├── Pod crashes
 ├── Scaling events
 ├── Restarts
 │
 │ (metrics / events)
 ▼
n8n
 ├── Evaluate logic
 ├── Notify Slack
 ├── Optional Git rollback
 │
 ▼
Git
 │
 ▼
Argo CD
 └── Re-sync cluster
</code></pre>
<hr />
<h2>What Makes This Production-Grade?</h2>
<ul>
<li><p>Self-healing pods</p>
</li>
<li><p>GitOps reconciliation</p>
</li>
<li><p>Auto-scaling</p>
</li>
<li><p>Internal service communication</p>
</li>
<li><p>Automated alerting</p>
</li>
<li><p>Event-driven workflows</p>
</li>
<li><p>Slack integration</p>
</li>
<li><p>Extensible automation engine</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Installing a Distributed Monitoring Platform: 3-VM Setup Process]]></title><description><![CDATA[System Architecture
Overview: Three-tier distributed system using separate VMs for application, monitoring, and logging - mimicking production infrastructure.

Why This Architecture:

Separation of Concerns: Each VM has a dedicated role (app/monitori...]]></description><link>https://blog.sachindu.me/installing-a-distributed-monitoring-platform-3-vm-setup-process</link><guid isPermaLink="true">https://blog.sachindu.me/installing-a-distributed-monitoring-platform-3-vm-setup-process</guid><category><![CDATA[Cloud Computing]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[Python]]></category><category><![CDATA[Grafana]]></category><category><![CDATA[#prometheus]]></category><category><![CDATA[elasticsearch]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[#nodeexporter]]></category><category><![CDATA[filebeat]]></category><category><![CDATA[observability]]></category><category><![CDATA[monitoring tool]]></category><category><![CDATA[Devops]]></category><category><![CDATA[SRE]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Mon, 09 Feb 2026 17:54:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770659886067/d6b67eb9-19e5-49ef-8060-254f8eda672d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-system-architecture">System Architecture</h2>
<p><strong>Overview:</strong> Three-tier distributed system using separate VMs for application, monitoring, and logging - mimicking production infrastructure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770660091861/6b114cf3-dff4-4e8e-8706-2811319b668d.png" alt class="image--center mx-auto" /></p>
<p><strong>Why This Architecture:</strong></p>
<ul>
<li><p><strong>Separation of Concerns:</strong> Each VM has a dedicated role (app/monitoring/logging)</p>
</li>
<li><p><strong>Scalability:</strong> Easy to scale each tier independently</p>
</li>
<li><p><strong>Observability Pillars:</strong> Covers metrics (Prometheus), logs (ELK), and visualization (Grafana/Kibana)</p>
</li>
</ul>
<h2 id="heading-setup-flow">Setup Flow</h2>
<p><strong>Purpose:</strong> Build infrastructure from golden image → deploy services → integrate monitoring/logging</p>
<pre><code class="lang-bash">Phase 1: VM Foundation
├── Create Golden Image Template (base OS with common tools)
├── Clone VMs (app-vm, monitoring-vm, logging-vm)
├── Fix Hostnames &amp; Machine IDs  ⚠️ [ERROR <span class="hljs-comment">#1] (prevent duplicate identity)</span>
├── Configure Static IPs (stable addressing <span class="hljs-keyword">for</span> monitoring)
└── Create Users &amp; Permissions (security &amp; access control)

Phase 2: Application VM Setup
├── Install Python &amp; Dependencies (runtime environment)
├── Create Flask Application (web app with metrics/logging)
├── Install Node Exporter (system-level metrics)
└── Install &amp; Configure Filebeat  ⚠️ [ERROR <span class="hljs-comment">#2, #3] (log shipper)</span>

Phase 3: Monitoring VM Setup
├── Install Prometheus (time-series metrics database)
├── Configure Scrape Targets (collect from app-vm)
├── Install Grafana (visualization &amp; dashboards)
└── Create Dashboards (display metrics)

Phase 4: Logging VM Setup
├── Install Elasticsearch (<span class="hljs-built_in">log</span> storage &amp; search)
├── Install Kibana (<span class="hljs-built_in">log</span> visualization)
├── Configure Data Views (index patterns)
└── Verify Log Ingestion (confirm data flow)

Phase 5: Integration &amp; Testing
├── Test Metrics Collection (Prometheus → Grafana)
├── Test Log Shipping (Filebeat → ELK)
└── Create Comprehensive Dashboards (unified view)
</code></pre>
<hr />
<h2 id="heading-detailed-setup-steps">Detailed Setup Steps</h2>
<h3 id="heading-phase-1-vm-foundation"><strong>PHASE 1: VM Foundation</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770523146645/bfc0e42c-c7b1-4cde-833d-79eee4eca3e2.png" alt class="image--center mx-auto" /></p>
<p><strong>Goal:</strong> Create reusable VM template and properly configure cloned instances with unique identities.</p>
<h4 id="heading-11-create-golden-image-template">1.1 Create Golden Image Template</h4>
<p><strong>Purpose:</strong> Single source of truth - install once, clone many times. Ensures consistency across all VMs.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install base Ubuntu Server 22.04</span>
sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install -y qemu-guest-agent curl wget vim htop net-tools
sudo systemctl <span class="hljs-built_in">enable</span> qemu-guest-agent

<span class="hljs-comment"># Optional: Install Docker</span>
sudo apt install -y docker docker-compose
</code></pre>
<h4 id="heading-12-clean-vm-before-cloning">1.2 Clean VM Before Cloning</h4>
<p><strong>Purpose:</strong> Remove machine-specific identifiers to prevent conflicts when cloning.</p>
<pre><code class="lang-bash">sudo cloud-init clean
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo poweroff
</code></pre>
<h4 id="heading-13-convert-to-template-in-proxmox-ui">1.3 Convert to Template in Proxmox UI</h4>
<ul>
<li><p>Right-click VM → Convert to Template</p>
</li>
<li><p><strong>Note:</strong> Template becomes read-only - cannot boot directly</p>
</li>
</ul>
<h4 id="heading-14-clone-vms">1.4 Clone VMs</h4>
<ul>
<li><p>Clone from template for: app-vm, monitoring-vm, logging-vm</p>
</li>
<li><p>Use Full Clone (recommended for independent VMs)</p>
</li>
<li><p><strong>Result:</strong> 3 identical VMs that need unique configuration</p>
</li>
</ul>
<hr />
<h3 id="heading-error-1-encountered-here"><strong>⚠️ ERROR #1 ENCOUNTERED HERE</strong></h3>
<p><strong>Why This Matters:</strong> Cloned VMs have identical hostnames and machine-ids, causing:</p>
<ul>
<li><p>Prometheus to see only 1 node instead of 3</p>
</li>
<li><p>Systemd service conflicts</p>
</li>
<li><p>Network confusion</p>
</li>
</ul>
<h4 id="heading-15-fix-hostnames-amp-machine-ids">1.5 Fix Hostnames &amp; Machine IDs</h4>
<p><strong>Purpose:</strong> Give each VM unique identity for proper monitoring and logging.</p>
<p><strong>App VM:</strong></p>
<pre><code class="lang-bash">sudo hostnamectl set-hostname app-vm
hostnamectl  <span class="hljs-comment"># Verify</span>
</code></pre>
<p><strong>Monitoring VM:</strong></p>
<pre><code class="lang-bash">sudo hostnamectl set-hostname monitoring-vm
hostnamectl  <span class="hljs-comment"># Verify</span>
</code></pre>
<p><strong>Logging VM:</strong></p>
<pre><code class="lang-bash">sudo hostnamectl set-hostname logging-vm
hostnamectl  <span class="hljs-comment"># Verify</span>
</code></pre>
<h4 id="heading-16-fix-etchosts-each-vm">1.6 Fix /etc/hosts (Each VM)</h4>
<p><strong>Purpose:</strong> Ensure hostname resolves correctly locally.</p>
<pre><code class="lang-bash">sudo nano /etc/hosts
</code></pre>
<p>Change:</p>
<pre><code class="lang-bash">127.0.1.1 app-server
</code></pre>
<p>To:</p>
<pre><code class="lang-bash">127.0.1.1 app-vm  <span class="hljs-comment"># (or monitoring-vm, logging-vm respectively)</span>
</code></pre>
<h4 id="heading-17-regenerate-machine-id-critical">1.7 Regenerate machine-id (CRITICAL)</h4>
<p><strong>Purpose:</strong> Create unique systemd identifier - required for proper journaling and service management. <strong>⚠️ Do NOT manually create IDs - let systemd generate them.</strong></p>
<pre><code class="lang-bash">sudo rm -f /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo systemd-machine-id-setup
cat /etc/machine-id  <span class="hljs-comment"># Verify unique ID</span>
sudo reboot
</code></pre>
<p><strong>After reboot, verify each VM has different machine-id</strong></p>
<h4 id="heading-18-create-common-user-all-vms">1.8 Create Common User (All VMs)</h4>
<p><strong>Purpose:</strong> Standard non-root user for application management and SSH access.</p>
<pre><code class="lang-bash">sudo useradd -m -s /bin/bash devops
sudo passwd devops
sudo usermod -aG sudo devops

<span class="hljs-comment"># Verify</span>
getent passwd devops
id devops
</code></pre>
<h4 id="heading-19-set-root-password-optional">1.9 Set Root Password (Optional)</h4>
<p><strong>Purpose:</strong> Enable root access for emergency situations (homelab only - disable in production).</p>
<pre><code class="lang-bash">sudo passwd -u root
sudo passwd root
</code></pre>
<hr />
<h4 id="heading-110-configure-static-ips">1.10 Configure Static IPs</h4>
<p><strong>Purpose:</strong> Fixed IPs are essential for monitoring systems - DHCP changes would break scrape targets and log shipping.</p>
<p><strong>Identify Network Interface:</strong></p>
<pre><code class="lang-bash">ip a  <span class="hljs-comment"># Note interface name (e.g., ens18)</span>
</code></pre>
<p><strong>Edit Netplan (Each VM):</strong></p>
<pre><code class="lang-bash">sudo nano /etc/netplan/00-installer-config.yaml
</code></pre>
<p><strong>App VM (192.168.8.50):</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">network:</span>
  <span class="hljs-attr">version:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">renderer:</span> <span class="hljs-string">networkd</span>
  <span class="hljs-attr">ethernets:</span>
    <span class="hljs-attr">ens18:</span>
      <span class="hljs-attr">dhcp4:</span> <span class="hljs-literal">no</span>
      <span class="hljs-attr">addresses:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-number">192.168</span><span class="hljs-number">.8</span><span class="hljs-number">.50</span><span class="hljs-string">/24</span>
      <span class="hljs-attr">gateway4:</span> <span class="hljs-number">192.168</span><span class="hljs-number">.8</span><span class="hljs-number">.1</span>
      <span class="hljs-attr">nameservers:</span>
        <span class="hljs-attr">addresses:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-number">8.8</span><span class="hljs-number">.8</span><span class="hljs-number">.8</span>
          <span class="hljs-bullet">-</span> <span class="hljs-number">1.1</span><span class="hljs-number">.1</span><span class="hljs-number">.1</span>
</code></pre>
<p><strong>Monitoring VM (192.168.8.60):</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">addresses:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-number">192.168</span><span class="hljs-number">.8</span><span class="hljs-number">.60</span><span class="hljs-string">/24</span>
<span class="hljs-comment"># (Everything else same)</span>
</code></pre>
<p><strong>Logging VM (192.168.8.70):</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">addresses:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-number">192.168</span><span class="hljs-number">.8</span><span class="hljs-number">.70</span><span class="hljs-string">/24</span>
<span class="hljs-comment"># (Everything else same)</span>
</code></pre>
<p><strong>Apply Configuration:</strong></p>
<pre><code class="lang-bash">sudo netplan apply
ip a  <span class="hljs-comment"># Verify</span>
ip route  <span class="hljs-comment"># Verify gateway</span>
</code></pre>
<p><strong>Test Connectivity:</strong></p>
<pre><code class="lang-bash">ping -c 3 192.168.8.1  <span class="hljs-comment"># Gateway</span>
ping 192.168.8.60      <span class="hljs-comment"># Monitoring VM</span>
ping 192.168.8.70      <span class="hljs-comment"># Logging VM</span>
</code></pre>
<p><strong>Expected:</strong> All pings successful = network ready</p>
<h4 id="heading-111-update-etchosts-all-vms">1.11 Update /etc/hosts (All VMs)</h4>
<p><strong>Purpose:</strong> Enable hostname-based communication between VMs (easier than remembering IPs).</p>
<pre><code class="lang-bash">sudo nano /etc/hosts
</code></pre>
<p>Add:</p>
<pre><code class="lang-bash">192.168.8.50 app-vm
192.168.8.60 monitoring-vm
192.168.8.70 logging-vm
</code></pre>
<p><strong>Test:</strong> <code>ping monitoring-vm</code> should work from any VM</p>
<hr />
<h3 id="heading-phase-2-application-vm-setup"><strong>PHASE 2: Application VM Setup</strong></h3>
<p><strong>Goal:</strong> Deploy Flask web application with Prometheus metrics export and JSON logging.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770523531054/499962a0-7312-4a94-95b5-88e1a05ac5c4.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-21-install-python-amp-dependencies">2.1 Install Python &amp; Dependencies</h4>
<p><strong>Purpose:</strong> Python runtime for Flask application.</p>
<pre><code class="lang-bash">ssh devops@app-vm
sudo apt update
sudo apt install -y python3 python3-pip python3-venv
</code></pre>
<h4 id="heading-22-create-application-directory">2.2 Create Application Directory</h4>
<p><strong>Purpose:</strong> Isolated virtual environment prevents dependency conflicts.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> ~
mkdir myapp
<span class="hljs-built_in">cd</span> myapp
python3 -m venv venv
<span class="hljs-built_in">source</span> venv/bin/activate
</code></pre>
<p><strong>Result:</strong> Shell prompt shows <code>(venv)</code> prefix</p>
<h4 id="heading-23-install-python-packages">2.3 Install Python Packages</h4>
<pre><code class="lang-bash">pip install flask prometheus-client
pip list  <span class="hljs-comment"># Verify</span>
</code></pre>
<h4 id="heading-24-create-flask-application">2.4 Create Flask Application</h4>
<p><strong>Purpose:</strong> Web app that:</p>
<ul>
<li><p>Serves HTTP requests</p>
</li>
<li><p>Exposes <code>/metrics</code> for Prometheus</p>
</li>
<li><p>Writes JSON logs for ELK</p>
</li>
</ul>
<pre><code class="lang-bash">nano app.py
</code></pre>
<p><strong>Paste the following code:</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> flask <span class="hljs-keyword">import</span> Flask, Response, render_template_string
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> random
<span class="hljs-keyword">import</span> logging
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> prometheus_client <span class="hljs-keyword">import</span> (
    Counter,
    Histogram,
    generate_latest,
    CONTENT_TYPE_LATEST
)

app = Flask(__name__)

<span class="hljs-comment"># ----------------------</span>
<span class="hljs-comment"># JSON Logging Setup</span>
<span class="hljs-comment"># ----------------------</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">JsonFormatter</span>(<span class="hljs-params">logging.Formatter</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">format</span>(<span class="hljs-params">self, record</span>):</span>
        log_record = {
            <span class="hljs-string">"@timestamp"</span>: datetime.utcnow().isoformat(),
            <span class="hljs-string">"log.level"</span>: record.levelname,
            <span class="hljs-string">"message"</span>: record.getMessage(),
            <span class="hljs-string">"logger"</span>: record.name
        }
        <span class="hljs-keyword">if</span> hasattr(record, <span class="hljs-string">"extra"</span>):
            log_record.update(record.extra)
        <span class="hljs-keyword">return</span> json.dumps(log_record)

handler = logging.FileHandler(<span class="hljs-string">"/var/log/myapp/app.log"</span>)
handler.setFormatter(JsonFormatter())
logger = logging.getLogger(<span class="hljs-string">"myapp"</span>)
logger.setLevel(logging.INFO)
logger.addHandler(handler)
logger.propagate = <span class="hljs-literal">False</span>

<span class="hljs-comment"># ----------------------</span>
<span class="hljs-comment"># Prometheus Metrics</span>
<span class="hljs-comment"># ----------------------</span>
REQUEST_COUNT = Counter(
    <span class="hljs-string">"http_requests_total"</span>,
    <span class="hljs-string">"Total HTTP requests"</span>,
    [<span class="hljs-string">"method"</span>, <span class="hljs-string">"endpoint"</span>, <span class="hljs-string">"status"</span>]
)

REQUEST_LATENCY = Histogram(
    <span class="hljs-string">"http_request_latency_seconds"</span>,
    <span class="hljs-string">"HTTP request latency in seconds"</span>,
    [<span class="hljs-string">"endpoint"</span>]
)

<span class="hljs-comment"># ----------------------</span>
<span class="hljs-comment"># HTML Template</span>
<span class="hljs-comment"># ----------------------</span>
HTML_TEMPLATE = <span class="hljs-string">"""
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;title&gt;MyApp - Distributed Monitoring Platform&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        html, body {
            height: 100%;
            overflow: hidden;
            font-family: 'Segoe UI', Arial, sans-serif;
        }
        body {
            background: linear-gradient(135deg, #0d47a1 0%, #1976d2 50%, #42a5f5 100%);
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .container {
            width: 95vw;
            height: 95vh;
            background: rgba(227, 242, 253, 0.95);
            border-radius: 15px;
            padding: 2vh 2vw;
            box-shadow: 0 15px 50px rgba(0, 0, 0, 0.3);
            display: flex;
            flex-direction: column;
        }
        header {
            text-align: center;
            padding-bottom: 1.5vh;
            border-bottom: 3px solid #1976d2;
            margin-bottom: 2vh;
        }
        h1 {
            font-size: 2.5vw;
            color: #0d47a1;
            margin-bottom: 0.5vh;
        }
        .tagline {
            font-size: 1.2vw;
            color: #1565c0;
        }
        .main-content {
            flex: 1;
            display: grid;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: auto 1fr;
            gap: 2vh;
            overflow: hidden;
        }
        .section {
            background: #bbdefb;
            padding: 2vh 1.5vw;
            border-radius: 10px;
            border-left: 5px solid #1976d2;
            overflow: auto;
        }
        .section h2 {
            color: #0d47a1;
            font-size: 1.5vw;
            margin-bottom: 1vh;
        }
        .section p, .section li {
            color: #1565c0;
            font-size: 1vw;
            line-height: 1.5;
        }
        .architecture {
            grid-column: 1 / -1;
            background: #90caf9;
        }
        .vm-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 1.5vw;
            margin-top: 1vh;
        }
        .vm-box {
            background: #e3f2fd;
            padding: 1.5vh 1vw;
            border-radius: 8px;
            border: 2px solid #1976d2;
            text-align: center;
        }
        .vm-box h3 {
            color: #0d47a1;
            font-size: 1.3vw;
            margin-bottom: 1vh;
        }
        .vm-box p {
            color: #1565c0;
            font-size: 0.9vw;
            margin: 0.5vh 0;
        }
        .vm-icon {
            font-size: 2.5vw;
            margin-bottom: 1vh;
        }
        .api-list {
            list-style: none;
        }
        .api-item {
            background: #e3f2fd;
            padding: 1vh 1vw;
            margin: 0.8vh 0;
            border-radius: 5px;
            border-left: 3px solid #1976d2;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .api-endpoint {
            font-weight: bold;
            color: #0d47a1;
            font-size: 1vw;
        }
        .api-method {
            background: #1976d2;
            color: white;
            padding: 0.3vh 0.8vw;
            border-radius: 3px;
            font-size: 0.8vw;
        }
        .sample-page {
            display: flex;
            flex-direction: column;
            gap: 1vh;
        }
        .sample-card {
            background: #e3f2fd;
            padding: 1vh 1vw;
            border-radius: 5px;
            border-left: 3px solid #1976d2;
        }
        .sample-card h4 {
            color: #0d47a1;
            font-size: 1.1vw;
            margin-bottom: 0.5vh;
        }
        .sample-card p {
            font-size: 0.9vw;
        }
        footer {
            text-align: center;
            padding-top: 1vh;
            border-top: 2px solid #1976d2;
            color: #1565c0;
            font-size: 0.9vw;
            margin-top: 1.5vh;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;header&gt;
            &lt;h1&gt;MyApp - Distributed Monitoring Platform&lt;/h1&gt;
            &lt;p class="tagline"&gt;Three-Tier Architecture | Application • Monitoring • Logging&lt;/p&gt;
        &lt;/header&gt;

        &lt;div class="main-content"&gt;
            &lt;div class="section architecture"&gt;
                &lt;h2&gt;🏗️ System Architecture&lt;/h2&gt;
                &lt;div class="vm-grid"&gt;
                    &lt;div class="vm-box"&gt;
                        &lt;div class="vm-icon"&gt;🖥️&lt;/div&gt;
                        &lt;h3&gt;App VM&lt;/h3&gt;
                        &lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Application Server&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Python Flask&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Port:&lt;/strong&gt; 5000&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt; REST APIs, Metrics Export&lt;/p&gt;
                    &lt;/div&gt;
                    &lt;div class="vm-box"&gt;
                        &lt;div class="vm-icon"&gt;📊&lt;/div&gt;
                        &lt;h3&gt;Monitor VM&lt;/h3&gt;
                        &lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Metrics &amp; Visualization&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Prometheus + Grafana&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Ports:&lt;/strong&gt; 9090, 3000&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt; Time-series DB, Dashboards&lt;/p&gt;
                    &lt;/div&gt;
                    &lt;div class="vm-box"&gt;
                        &lt;div class="vm-icon"&gt;📝&lt;/div&gt;
                        &lt;h3&gt;Logging VM&lt;/h3&gt;
                        &lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Log Aggregation&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Elasticsearch + Kibana&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Ports:&lt;/strong&gt; 9200, 5601&lt;/p&gt;
                        &lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt; Log Search, Analysis&lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="section"&gt;
                &lt;h2&gt;🔌 Available APIs&lt;/h2&gt;
                &lt;ul class="api-list"&gt;
                    &lt;li class="api-item"&gt;
                        &lt;span class="api-endpoint"&gt;/&lt;/span&gt;
                        &lt;span class="api-method"&gt;GET&lt;/span&gt;
                    &lt;/li&gt;
                    &lt;li class="api-item"&gt;
                        &lt;span class="api-endpoint"&gt;/api&lt;/span&gt;
                        &lt;span class="api-method"&gt;GET&lt;/span&gt;
                    &lt;/li&gt;
                    &lt;li class="api-item"&gt;
                        &lt;span class="api-endpoint"&gt;/slow&lt;/span&gt;
                        &lt;span class="api-method"&gt;GET&lt;/span&gt;
                    &lt;/li&gt;
                    &lt;li class="api-item"&gt;
                        &lt;span class="api-endpoint"&gt;/error&lt;/span&gt;
                        &lt;span class="api-method"&gt;GET&lt;/span&gt;
                    &lt;/li&gt;
                    &lt;li class="api-item"&gt;
                        &lt;span class="api-endpoint"&gt;/metrics&lt;/span&gt;
                        &lt;span class="api-method"&gt;GET&lt;/span&gt;
                    &lt;/li&gt;
                &lt;/ul&gt;
            &lt;/div&gt;

            &lt;div class="section sample-page"&gt;
                &lt;h2&gt;📄 Sample Page&lt;/h2&gt;
                &lt;div class="sample-card"&gt;
                    &lt;h4&gt;Application Features&lt;/h4&gt;
                    &lt;p&gt;Real-time monitoring with Prometheus metrics collection and Grafana visualization&lt;/p&gt;
                &lt;/div&gt;
                &lt;div class="sample-card"&gt;
                    &lt;h4&gt;Logging System&lt;/h4&gt;
                    &lt;p&gt;Centralized log management using Elasticsearch with Kibana dashboards&lt;/p&gt;
                &lt;/div&gt;
                &lt;div class="sample-card"&gt;
                    &lt;h4&gt;Performance Tracking&lt;/h4&gt;
                    &lt;p&gt;Request latency, error rates, and throughput metrics tracked across all endpoints&lt;/p&gt;
                &lt;/div&gt;
                &lt;div class="sample-card"&gt;
                    &lt;h4&gt;Distributed Architecture&lt;/h4&gt;
                    &lt;p&gt;Scalable three-tier setup with dedicated VMs for app, monitoring, and logging&lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;footer&gt;
            &lt;p&gt;🚀 MyApp v1.0 | Powered by Flask • Prometheus • Grafana • Elasticsearch • Kibana | Status: ✅ Running&lt;/p&gt;
        &lt;/footer&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
"""</span>

<span class="hljs-comment"># ----------------------</span>
<span class="hljs-comment"># Routes</span>
<span class="hljs-comment"># ----------------------</span>
<span class="hljs-meta">@app.route("/")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">home</span>():</span>
    start_time = time.time()
    REQUEST_COUNT.labels(<span class="hljs-string">"GET"</span>, <span class="hljs-string">"/"</span>, <span class="hljs-string">"200"</span>).inc()
    latency = time.time() - start_time
    REQUEST_LATENCY.labels(<span class="hljs-string">"/"</span>).observe(latency)

    logger.info(
        <span class="hljs-string">"request_completed"</span>,
        extra={
            <span class="hljs-string">"endpoint"</span>: <span class="hljs-string">"/"</span>,
            <span class="hljs-string">"method"</span>: <span class="hljs-string">"GET"</span>,
            <span class="hljs-string">"status"</span>: <span class="hljs-number">200</span>,
            <span class="hljs-string">"latency_ms"</span>: round(latency * <span class="hljs-number">1000</span>, <span class="hljs-number">2</span>)
        }
    )

    <span class="hljs-keyword">return</span> render_template_string(HTML_TEMPLATE)

<span class="hljs-meta">@app.route("/api")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">api</span>():</span>
    start_time = time.time()
    REQUEST_COUNT.labels(<span class="hljs-string">"GET"</span>, <span class="hljs-string">"/api"</span>, <span class="hljs-string">"200"</span>).inc()
    latency = time.time() - start_time
    REQUEST_LATENCY.labels(<span class="hljs-string">"/api"</span>).observe(latency)

    logger.info(
        <span class="hljs-string">"request_completed"</span>,
        extra={
            <span class="hljs-string">"endpoint"</span>: <span class="hljs-string">"/api"</span>,
            <span class="hljs-string">"method"</span>: <span class="hljs-string">"GET"</span>,
            <span class="hljs-string">"status"</span>: <span class="hljs-number">200</span>,
            <span class="hljs-string">"latency_ms"</span>: round(latency * <span class="hljs-number">1000</span>, <span class="hljs-number">2</span>)
        }
    )

    <span class="hljs-keyword">return</span> <span class="hljs-string">"API is running\n"</span>

<span class="hljs-meta">@app.route("/slow")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">slow</span>():</span>
    delay = random.uniform(<span class="hljs-number">1</span>, <span class="hljs-number">4</span>)
    time.sleep(delay)
    REQUEST_COUNT.labels(<span class="hljs-string">"GET"</span>, <span class="hljs-string">"/slow"</span>, <span class="hljs-string">"200"</span>).inc()
    REQUEST_LATENCY.labels(<span class="hljs-string">"/slow"</span>).observe(delay)

    logger.warning(
        <span class="hljs-string">"slow_request"</span>,
        extra={
            <span class="hljs-string">"endpoint"</span>: <span class="hljs-string">"/slow"</span>,
            <span class="hljs-string">"method"</span>: <span class="hljs-string">"GET"</span>,
            <span class="hljs-string">"status"</span>: <span class="hljs-number">200</span>,
            <span class="hljs-string">"latency_ms"</span>: round(delay * <span class="hljs-number">1000</span>, <span class="hljs-number">2</span>)
        }
    )

    <span class="hljs-keyword">return</span> <span class="hljs-string">f"Slow response: <span class="hljs-subst">{delay:<span class="hljs-number">.2</span>f}</span>s\n"</span>

<span class="hljs-meta">@app.route("/error")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">error</span>():</span>
    REQUEST_COUNT.labels(<span class="hljs-string">"GET"</span>, <span class="hljs-string">"/error"</span>, <span class="hljs-string">"500"</span>).inc()

    logger.error(
        <span class="hljs-string">"application_error"</span>,
        extra={
            <span class="hljs-string">"endpoint"</span>: <span class="hljs-string">"/error"</span>,
            <span class="hljs-string">"method"</span>: <span class="hljs-string">"GET"</span>,
            <span class="hljs-string">"status"</span>: <span class="hljs-number">500</span>
        }
    )

    <span class="hljs-keyword">return</span> <span class="hljs-string">"Error occurred\n"</span>, <span class="hljs-number">500</span>

<span class="hljs-meta">@app.route("/metrics")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">metrics</span>():</span>
    <span class="hljs-keyword">return</span> Response(
        generate_latest(),
        mimetype=CONTENT_TYPE_LATEST
    )

<span class="hljs-comment"># ----------------------</span>
<span class="hljs-comment"># Application Entry</span>
<span class="hljs-comment"># ----------------------</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    app.run(host=<span class="hljs-string">"0.0.0.0"</span>, port=<span class="hljs-number">5000</span>)
</code></pre>
<p><strong>App Features:</strong></p>
<ul>
<li><p><code>/</code> - Home page with system info</p>
</li>
<li><p><code>/api</code> - Simple API endpoint</p>
</li>
<li><p><code>/slow</code> - Simulates slow requests (1-4s)</p>
</li>
<li><p><code>/error</code> - Returns 500 error</p>
</li>
<li><p><code>/metrics</code> - Prometheus metrics endpoint</p>
</li>
</ul>
<h4 id="heading-25-create-log-directory">2.5 Create Log Directory</h4>
<p><strong>Purpose:</strong> Application needs write permissions for log file.</p>
<pre><code class="lang-bash">sudo mkdir -p /var/<span class="hljs-built_in">log</span>/myapp
sudo chown -R devops:devops /var/<span class="hljs-built_in">log</span>/myapp
</code></pre>
<h4 id="heading-26-test-application-manually">2.6 Test Application Manually</h4>
<p><strong>Purpose:</strong> Verify app works before creating systemd service.</p>
<pre><code class="lang-bash">python3 app.py
</code></pre>
<p><strong>From your laptop:</strong></p>
<pre><code class="lang-bash">curl http://192.168.8.50:5000
curl http://192.168.8.50:5000/metrics
cat /var/<span class="hljs-built_in">log</span>/myapp/app.log  <span class="hljs-comment"># Verify logs</span>
</code></pre>
<p><strong>Expected:</strong> HTTP responses and JSON logs being written</p>
<hr />
<h3 id="heading-error-3-encountered-here"><strong>⚠️ ERROR #3 ENCOUNTERED HERE</strong></h3>
<h4 id="heading-27-create-systemd-service">2.7 Create Systemd Service</h4>
<p><strong>Purpose:</strong> Auto-start Flask app on boot and keep it running. Production standard vs manual <code>python</code> <a target="_blank" href="http://app.py"><code>app.py</code></a>.</p>
<pre><code class="lang-bash">sudo nano /etc/systemd/system/myapp.service
</code></pre>
<p><strong>Paste:</strong></p>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=MyApp Flask Application
<span class="hljs-attr">After</span>=network.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=simple
<span class="hljs-attr">User</span>=devops
<span class="hljs-attr">Group</span>=devops
<span class="hljs-attr">WorkingDirectory</span>=/home/devops/myapp
<span class="hljs-attr">ExecStart</span>=/home/devops/myapp/venv/bin/python3 /home/devops/myapp/app.py

<span class="hljs-attr">Restart</span>=always
<span class="hljs-attr">RestartSec</span>=<span class="hljs-number">5</span>

<span class="hljs-attr">StandardOutput</span>=journal
<span class="hljs-attr">StandardError</span>=journal
<span class="hljs-attr">SyslogIdentifier</span>=myapp

<span class="hljs-attr">NoNewPrivileges</span>=<span class="hljs-literal">true</span>

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<p><strong>Critical Line:</strong> <code>ExecStart</code> must point to venv Python, not system Python (see <mark>Error #3</mark>)</p>
<p><strong>Enable and Start:</strong></p>
<pre><code class="lang-bash">sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> myapp.service
sudo systemctl start myapp.service
sudo systemctl status myapp.service
</code></pre>
<p><strong>Expected:</strong> Status shows "active (running)"</p>
<hr />
<h4 id="heading-28-install-node-exporter">2.8 Install Node Exporter</h4>
<p><strong>Purpose:</strong> Export system metrics (CPU, memory, disk) to Prometheus - app metrics come from Flask, system metrics from Node Exporter.</p>
<pre><code class="lang-bash">wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
tar xvf node_exporter-1.6.1.linux-amd64.tar.gz
sudo mv node_exporter-1.6.1.linux-amd64/node_exporter /usr/<span class="hljs-built_in">local</span>/bin/
sudo useradd --no-create-home --shell /bin/<span class="hljs-literal">false</span> node_exporter
</code></pre>
<p><strong>Create Service:</strong></p>
<pre><code class="lang-bash">sudo nano /etc/systemd/system/node_exporter.service
</code></pre>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Node Exporter
<span class="hljs-attr">After</span>=network.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">User</span>=node_exporter
<span class="hljs-attr">Group</span>=node_exporter
<span class="hljs-attr">Type</span>=simple
<span class="hljs-attr">ExecStart</span>=/usr/local/bin/node_exporter

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<p><strong>Start Service:</strong></p>
<pre><code class="lang-bash">sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> node_exporter
sudo systemctl start node_exporter
curl http://localhost:9100/metrics  <span class="hljs-comment"># Verify</span>
</code></pre>
<p><strong>Expected:</strong> Hundreds of metrics like <code>node_cpu_seconds_total</code>, <code>node_memory_MemAvailable_bytes</code></p>
<hr />
<h3 id="heading-error-2-encountered-here"><strong>⚠️ ERROR #2 ENCOUNTERED HERE</strong></h3>
<h4 id="heading-29-install-filebeat">2.9 Install Filebeat</h4>
<p><strong>Purpose:</strong> Lightweight log shipper - tails log files and sends to Elasticsearch. Part of the Elastic Stack.</p>
<p><strong>Issue:</strong> Filebeat not in standard Ubuntu repos - requires Elastic repository.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Fix apt repositories first</span>
sudo apt install -y apt-transport-https curl gnupg
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | \
sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
<span class="hljs-built_in">echo</span> <span class="hljs-string">"deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main"</span> | \
sudo tee /etc/apt/sources.list.d/elastic-8.x.list
sudo apt update
sudo apt install filebeat -y
</code></pre>
<h4 id="heading-210-configure-filebeat">2.10 Configure Filebeat</h4>
<p><strong>Purpose:</strong> Tell Filebeat what to read (app.log) and where to send (Elasticsearch on logging-vm).</p>
<pre><code class="lang-bash">sudo nano /etc/filebeat/filebeat.yml
</code></pre>
<p><strong>Minimal Config:</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">filebeat.inputs:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">log</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">paths:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">/var/log/myapp/app.log</span>
  <span class="hljs-attr">fields:</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">my-python-app</span>
  <span class="hljs-attr">fields_under_root:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">output.elasticsearch:</span>
  <span class="hljs-attr">hosts:</span> [<span class="hljs-string">"http://&lt;ELK_VM_IP&gt;:9200"</span>]

<span class="hljs-attr">setup.kibana:</span>
  <span class="hljs-attr">host:</span> <span class="hljs-string">"http://&lt;ELK_VM_IP&gt;:5601"</span>
</code></pre>
<p><strong>Key Points:</strong></p>
<ul>
<li><p>Input: Monitor <code>/var/log/myapp/app.log</code></p>
</li>
<li><p>Output: Send to Elasticsearch at 192.168.8.70:9200</p>
</li>
</ul>
<p><strong>Start Filebeat:</strong></p>
<pre><code class="lang-bash">sudo systemctl <span class="hljs-built_in">enable</span> filebeat
sudo systemctl start filebeat
sudo journalctl -u filebeat -f  <span class="hljs-comment"># Monitor logs</span>
</code></pre>
<p><strong>Expected Output:</strong></p>
<ul>
<li><p>"Publishing events"</p>
</li>
<li><p>"Connection to Elasticsearch established"</p>
</li>
<li><p>No "connection refused" errors</p>
</li>
</ul>
<hr />
<h3 id="heading-phase-3-monitoring-vm-setup"><strong>PHASE 3: Monitoring VM Setup</strong></h3>
<p><strong>Goal:</strong> Deploy Prometheus (metrics storage) and Grafana (visualization) to monitor app-vm.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770524845924/9d82a395-3e91-4bb9-9669-632ab1f11926.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-31-install-prometheus">3.1 Install Prometheus</h4>
<p><strong>Purpose:</strong> Time-series database that pulls metrics from app-vm every 15 seconds. Industry standard for metrics.</p>
<pre><code class="lang-bash">ssh devops@monitoring-vm
sudo apt update &amp;&amp; sudo apt upgrade -y
sudo useradd --no-create-home --shell /bin/<span class="hljs-literal">false</span> prometheus
</code></pre>
<p><strong>Download and Install:</strong></p>
<pre><code class="lang-bash">wget https://github.com/prometheus/prometheus/releases/download/v2.47.0/prometheus-2.47.0.linux-amd64.tar.gz
tar xvf prometheus-2.47.0.linux-amd64.tar.gz
sudo mv prometheus-2.47.0.linux-amd64/prometheus /usr/<span class="hljs-built_in">local</span>/bin/
sudo mv prometheus-2.47.0.linux-amd64/promtool /usr/<span class="hljs-built_in">local</span>/bin/
</code></pre>
<p><strong>Create Directories:</strong></p>
<pre><code class="lang-bash">sudo mkdir /etc/prometheus
sudo mv prometheus-2.47.0.linux-amd64/consoles /etc/prometheus/
sudo mv prometheus-2.47.0.linux-amd64/console_libraries /etc/prometheus/
sudo mv prometheus-2.47.0.linux-amd64/prometheus.yml /etc/prometheus/
sudo chown -R prometheus:prometheus /etc/prometheus
sudo chown prometheus:prometheus /usr/<span class="hljs-built_in">local</span>/bin/prometheus /usr/<span class="hljs-built_in">local</span>/bin/promtool
</code></pre>
<h4 id="heading-32-configure-prometheus">3.2 Configure Prometheus</h4>
<p><strong>Purpose:</strong> Define scrape targets - tell Prometheus where to collect metrics from.</p>
<pre><code class="lang-bash">sudo nano /etc/prometheus/prometheus.yml
</code></pre>
<p><strong>Add Scrape Targets:</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">15s</span>

<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'flask_app'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'192.168.8.50:5000'</span>]

  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'node_exporter'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'192.168.8.50:9100'</span>]
</code></pre>
<p><strong>What This Does:</strong></p>
<ul>
<li><p>Every 15s, scrape <code>192.168.8.50:5000/metrics</code> (Flask app metrics)</p>
</li>
<li><p>Every 15s, scrape <code>192.168.8.50:9100/metrics</code> (system metrics)</p>
</li>
<li><p>Store data in time-series database</p>
</li>
</ul>
<h4 id="heading-33-create-prometheus-service">3.3 Create Prometheus Service</h4>
<pre><code class="lang-bash">sudo nano /etc/systemd/system/prometheus.service
</code></pre>
<pre><code class="lang-ini"><span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Prometheus Monitoring
<span class="hljs-attr">Wants</span>=network-<span class="hljs-literal">on</span>line.target
<span class="hljs-attr">After</span>=network-<span class="hljs-literal">on</span>line.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">User</span>=prometheus
<span class="hljs-attr">Group</span>=prometheus
<span class="hljs-attr">Type</span>=simple
<span class="hljs-attr">ExecStart</span>=/usr/local/bin/prometheus \
  --config.file /etc/prometheus/prometheus.yml \
  --storage.tsdb.path /var/lib/prometheus/ \
  <span class="hljs-attr">--web.console.templates</span>=/etc/prometheus/consoles \
  <span class="hljs-attr">--web.console.libraries</span>=/etc/prometheus/console_libraries

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre>
<p><strong>Create Storage &amp; Start:</strong></p>
<pre><code class="lang-bash">sudo mkdir /var/lib/prometheus
sudo chown prometheus:prometheus /var/lib/prometheus
sudo systemctl daemon-reload
sudo systemctl start prometheus
sudo systemctl <span class="hljs-built_in">enable</span> prometheus
sudo systemctl status prometheus
</code></pre>
<p><strong>Test:</strong> Open <a target="_blank" href="http://192.168.8.60:9090"><code>http://192.168.8.60:9090</code></a></p>
<ul>
<li><p>Go to Status → Targets</p>
</li>
<li><p>Both targets should be "UP"</p>
</li>
</ul>
<hr />
<h4 id="heading-34-install-grafana">3.4 Install Grafana</h4>
<p><strong>Purpose:</strong> Visualization layer on top of Prometheus - creates beautiful dashboards from metrics.</p>
<pre><code class="lang-bash">sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository <span class="hljs-string">"deb https://packages.grafana.com/oss/deb stable main"</span>
sudo apt update
sudo apt install -y grafana
</code></pre>
<p><strong>Start Grafana:</strong></p>
<pre><code class="lang-bash">sudo systemctl start grafana-server
sudo systemctl <span class="hljs-built_in">enable</span> grafana-server
sudo systemctl status grafana-server
</code></pre>
<p><strong>Access:</strong> <a target="_blank" href="http://192.168.8.60:3000"><code>http://192.168.8.60:3000</code></a></p>
<ul>
<li><p>Default login: <code>admin/admin</code></p>
</li>
<li><p>You'll be prompted to set new password</p>
</li>
</ul>
<h4 id="heading-35-configure-grafana">3.5 Configure Grafana</h4>
<p><strong>1. Add Prometheus Data Source:</strong></p>
<ul>
<li><p>Settings → Data Sources → Add data source</p>
</li>
<li><p>Select <strong>Prometheus</strong></p>
</li>
<li><p>URL: <a target="_blank" href="http://localhost:9090"><code>http://localhost:9090</code></a></p>
</li>
<li><p>Click <strong>Save &amp; Test</strong> (should show green checkmark)</p>
</li>
</ul>
<p><strong>2. Import Node Exporter Dashboard:</strong></p>
<ul>
<li><p>Dashboards → Import</p>
</li>
<li><p>Dashboard ID: <strong>1860</strong> (Node Exporter Full)</p>
</li>
<li><p>Select Prometheus data source</p>
</li>
<li><p>Import</p>
</li>
</ul>
<p><strong>Result:</strong> System metrics dashboard (CPU, memory, disk, network) for app-vm</p>
<p><strong>3. Create Custom Flask Dashboard:</strong></p>
<p><strong>Purpose:</strong> Monitor application-specific metrics not covered by Node Exporter.</p>
<ul>
<li><p>Create new dashboard</p>
</li>
<li><p>Add panel with queries:</p>
</li>
</ul>
<p><strong>Request Rate:</strong></p>
<pre><code class="lang-bash">rate(http_requests_total[1m])
</code></pre>
<p><strong>Latency (95th percentile):</strong></p>
<pre><code class="lang-bash">histogram_quantile(0.95, sum(rate(http_request_latency_seconds_bucket[5m])) by (le))
</code></pre>
<p><strong>Error Rate:</strong></p>
<pre><code class="lang-bash">rate(http_requests_total{status=<span class="hljs-string">"500"</span>}[1m])
</code></pre>
<p><strong>Why Two Dashboards:</strong></p>
<ul>
<li><p>Dashboard 1860: System health (CPU, RAM, disk)</p>
</li>
<li><p>Custom dashboard: App health (requests, latency, errors)</p>
</li>
</ul>
<hr />
<h3 id="heading-phase-4-logging-vm-setup"><strong>PHASE 4: Logging VM Setup</strong></h3>
<p><strong>Goal:</strong> Deploy ELK stack (Elasticsearch + Kibana) for centralized log management and analysis.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770657720970/8ea6d760-eefe-4620-8490-758def1f31f3.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-41-install-elasticsearch">4.1 Install Elasticsearch</h4>
<p><strong>Purpose:</strong> Scalable search engine - stores and indexes logs for fast querying. Core of the ELK stack.</p>
<pre><code class="lang-bash">ssh devops@logging-vm
sudo apt update &amp;&amp; sudo apt upgrade -y

<span class="hljs-comment"># Add Elastic repo</span>
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
<span class="hljs-built_in">echo</span> <span class="hljs-string">"deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main"</span> | \
sudo tee /etc/apt/sources.list.d/elastic-8.x.list
sudo apt update
sudo apt install elasticsearch -y
</code></pre>
<h4 id="heading-42-configure-elasticsearch">4.2 Configure Elasticsearch</h4>
<p><strong>Purpose:</strong> Make Elasticsearch accessible from network and disable security for homelab simplicity.</p>
<pre><code class="lang-bash">sudo nano /etc/elasticsearch/elasticsearch.yml
</code></pre>
<p><strong>Set:</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">cluster.name:</span> <span class="hljs-string">my-logging-cluster</span>
<span class="hljs-attr">node.name:</span> <span class="hljs-string">logging-node-1</span>

<span class="hljs-attr">network.host:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>
<span class="hljs-attr">http.port:</span> <span class="hljs-number">9200</span>

<span class="hljs-attr">discovery.type:</span> <span class="hljs-string">single-node</span>

<span class="hljs-attr">xpack.security.enabled:</span> <span class="hljs-literal">false</span>
</code></pre>
<p><strong>Configuration Explained:</strong></p>
<ul>
<li><p><code>network.host: 0.0.0.0</code> - Accept connections from any IP</p>
</li>
<li><p><code>discovery.type: single-node</code> - Not clustering (single VM)</p>
</li>
<li><p><code>xpack.security.enabled: false</code> - Disable auth (⚠️ production should enable)</p>
</li>
</ul>
<p><strong>Start Elasticsearch:</strong></p>
<pre><code class="lang-bash">sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> elasticsearch
sudo systemctl start elasticsearch
curl http://localhost:9200  <span class="hljs-comment"># Verify</span>
</code></pre>
<p><strong>Expected:</strong> JSON response with cluster name, version info</p>
<hr />
<h4 id="heading-43-install-kibana">4.3 Install Kibana</h4>
<p><strong>Purpose:</strong> Web UI for Elasticsearch - search, visualize, and analyze logs through dashboards.</p>
<pre><code class="lang-bash">sudo apt install kibana -y
</code></pre>
<p><strong>Configure:</strong></p>
<pre><code class="lang-bash">sudo nano /etc/kibana/kibana.yml
</code></pre>
<pre><code class="lang-yaml"><span class="hljs-attr">server.port:</span> <span class="hljs-number">5601</span>
<span class="hljs-attr">server.host:</span> <span class="hljs-string">"0.0.0.0"</span>

<span class="hljs-attr">elasticsearch.hosts:</span> [<span class="hljs-string">"http://localhost:9200"</span>]
</code></pre>
<p><strong>Start Kibana:</strong></p>
<pre><code class="lang-bash">sudo systemctl <span class="hljs-built_in">enable</span> kibana
sudo systemctl start kibana
sudo systemctl status kibana
</code></pre>
<p><strong>Access:</strong> <code>http://192.168.8.70:5601</code></p>
<ul>
<li>Initial load may take 1-2 minutes</li>
</ul>
<hr />
<h4 id="heading-44-create-data-view-in-kibana">4.4 Create Data View in Kibana</h4>
<p><strong>Purpose:</strong> Tell Kibana which Elasticsearch indices to query. Filebeat creates indices like <code>filebeat-2026.02.07</code>.</p>
<ol>
<li><p>Go to <strong>Stack Management → Data Views</strong></p>
</li>
<li><p>Click <strong>Create data view</strong></p>
</li>
<li><p>Fill in:</p>
<ul>
<li><p>Name: <code>filebeat-myapp</code></p>
</li>
<li><p>Index pattern: <code>filebeat-*</code> (matches all filebeat indices)</p>
</li>
<li><p>Time field: <code>@timestamp</code></p>
</li>
</ul>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
</ol>
<p><strong>Why</strong> <code>filebeat-*</code> Pattern:</p>
<ul>
<li><p>Filebeat creates daily indices: <code>filebeat-2026.02.07</code>, <code>filebeat-2026.02.08</code>, etc.</p>
</li>
<li><p>Wildcard <code>*</code> matches all of them</p>
</li>
<li><p>New indices auto-included</p>
</li>
</ul>
<h4 id="heading-45-view-logs">4.5 View Logs</h4>
<p><strong>Purpose:</strong> Verify logs are flowing from app-vm → Filebeat → Elasticsearch → Kibana.</p>
<ul>
<li><p>Navigate to <strong>Discover</strong></p>
</li>
<li><p>Select data view: <code>filebeat-myapp</code></p>
</li>
<li><p>You should see JSON logs with fields:</p>
<ul>
<li><p><code>@timestamp</code> - When log was created</p>
</li>
<li><p><code>log.level</code> - INFO, WARNING, ERROR</p>
</li>
<li><p><code>message</code> - Log message</p>
</li>
<li><p><code>endpoint</code> - Which API endpoint</p>
</li>
<li><p><code>latency_ms</code> - Request duration</p>
</li>
</ul>
</li>
</ul>
<p><strong>If No Logs Appear:</strong></p>
<ul>
<li><p>Check Filebeat status on app-vm: <code>sudo systemctl status filebeat</code></p>
</li>
<li><p>Check Elasticsearch indices: <code>curl</code> <code>http://192.168.8.70:9200/_cat/indices?v</code></p>
</li>
<li><p>Look for <code>filebeat-*</code> indices</p>
</li>
</ul>
<hr />
<h3 id="heading-phase-5-integration-amp-testing"><strong>PHASE 5: Integration &amp; Testing</strong></h3>
<p><strong>Goal:</strong> Verify complete data flow and create comprehensive monitoring dashboards.</p>
<h4 id="heading-51-test-complete-flow">5.1 Test Complete Flow</h4>
<p><strong>Purpose:</strong> Generate realistic traffic to produce metrics and logs for visualization.</p>
<p><strong>Generate Traffic:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># From your laptop</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..100}; <span class="hljs-keyword">do</span> curl http://192.168.8.50:5000/; <span class="hljs-keyword">done</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..50}; <span class="hljs-keyword">do</span> curl http://192.168.8.50:5000/slow; <span class="hljs-keyword">done</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..20}; <span class="hljs-keyword">do</span> curl http://192.168.8.50:5000/error; <span class="hljs-keyword">done</span>
</code></pre>
<p><strong>What This Creates:</strong></p>
<ul>
<li><p>100 normal requests → <code>http_requests_total</code> metric increments</p>
</li>
<li><p>50 slow requests → <code>http_request_latency_seconds</code> histogram data</p>
</li>
<li><p>20 errors → ERROR level logs in Kibana</p>
</li>
</ul>
<h4 id="heading-52-verify-metrics-in-grafana">5.2 Verify Metrics in Grafana</h4>
<p><strong>Purpose:</strong> Confirm Prometheus is scraping and Grafana is displaying metrics.</p>
<ul>
<li><p>Check <code>http://192.168.8.60:3000</code></p>
</li>
<li><p>Verify dashboards show:</p>
<ul>
<li><p><strong>Request rate:</strong> Should spike during traffic generation</p>
</li>
<li><p><strong>Latency percentiles:</strong> <code>/slow</code> endpoint shows higher latency</p>
</li>
<li><p><strong>Error rate:</strong> Spike from <code>/error</code> requests</p>
</li>
<li><p><strong>System metrics:</strong> CPU/memory usage from Node Exporter (Dashboard 1860)</p>
</li>
</ul>
</li>
</ul>
<p><strong>Queries to Verify:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># In Grafana Explore</span>
rate(http_requests_total[1m])           <span class="hljs-comment"># Should show recent activity</span>
http_request_latency_seconds{quantile=<span class="hljs-string">"0.95"</span>}  <span class="hljs-comment"># Should be higher for /slow</span>
</code></pre>
<h4 id="heading-53-verify-logs-in-kibana">5.3 Verify Logs in Kibana</h4>
<p><strong>Purpose:</strong> Confirm Filebeat → Elasticsearch → Kibana pipeline is working.</p>
<ul>
<li><p>Check <code>http://192.168.8.70:5601</code></p>
</li>
<li><p>Go to <strong>Discover</strong> → Select <code>filebeat-myapp</code></p>
</li>
</ul>
<p><strong>Search Examples:</strong></p>
<pre><code class="lang-bash">log.level: ERROR                    <span class="hljs-comment"># Find all errors</span>
endpoint: <span class="hljs-string">"/slow"</span>                   <span class="hljs-comment"># Find slow requests</span>
latency_ms &gt; 1000                   <span class="hljs-comment"># Requests over 1 second</span>
</code></pre>
<p><strong>Create Visualizations:</strong></p>
<ol>
<li><p><strong>Errors Over Time:</strong></p>
<ul>
<li><p>Lens → Line chart</p>
</li>
<li><p>Filter: <code>log.level : "ERROR"</code></p>
</li>
<li><p>X-axis: <code>@timestamp</code></p>
</li>
</ul>
</li>
<li><p><strong>Requests by Endpoint:</strong></p>
<ul>
<li><p>Lens → Bar chart</p>
</li>
<li><p>Y-axis: Count</p>
</li>
<li><p>X-axis: <code>endpoint.keyword</code></p>
</li>
</ul>
</li>
<li><p><strong>Latency Distribution:</strong></p>
<ul>
<li><p>Filter: <code>latency_ms</code> exists</p>
</li>
<li><p>Histogram of latency values</p>
</li>
</ul>
</li>
</ol>
<p><strong>Save to Dashboard:</strong> Combine visualizations into unified logging dashboard</p>
<hr />
<h2 id="heading-errors-amp-solutions-summary">⚠️ Errors &amp; Solutions Summary</h2>
<h3 id="heading-error-1-duplicate-machine-ids-amp-hostnames-after-cloning"><strong>ERROR #1: Duplicate Machine IDs &amp; Hostnames After Cloning</strong></h3>
<p><strong>Symptom:</strong> All VMs report same hostname and machine-id after cloning from template.</p>
<p><strong>Why This Happens:</strong> Proxmox clones EVERYTHING including <code>/etc/machine-id</code>, <code>/etc/hostname</code>, and system identifiers.</p>
<p><strong>Impact:</strong></p>
<ul>
<li><p>Prometheus sees only 1 node instead of 3 (metrics collision)</p>
</li>
<li><p>Systemd services conflict</p>
</li>
<li><p>Logs from all VMs appear to come from same source</p>
</li>
<li><p>Network confusion in monitoring tools</p>
</li>
</ul>
<p><strong>Root Cause:</strong> Machine-specific files were copied during clone operation.</p>
<p><strong>Solution:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Fix hostname</span>
sudo hostnamectl set-hostname &lt;vm-name&gt;

<span class="hljs-comment"># Fix /etc/hosts</span>
sudo nano /etc/hosts
<span class="hljs-comment"># Change 127.0.1.1 to correct hostname</span>

<span class="hljs-comment"># Regenerate machine-id (CRITICAL - must be systemd-generated)</span>
sudo rm -f /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo systemd-machine-id-setup
sudo reboot
</code></pre>
<p><strong>Verification:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># On each VM, these should be DIFFERENT:</span>
hostnamectl
cat /etc/machine-id
</code></pre>
<p><strong>Why This is Critical for Observability:</strong></p>
<ul>
<li><p>Prometheus labels nodes by machine-id</p>
</li>
<li><p>Grafana dashboards group by hostname</p>
</li>
<li><p>ELK logs tagged with <a target="_blank" href="http://host.name">host.name</a></p>
</li>
<li><p>Without unique IDs, all data collapses into single source</p>
</li>
</ul>
<hr />
<h3 id="heading-error-2-apt-timeout-when-installing-filebeat"><strong>ERROR #2: Apt Timeout When Installing Filebeat</strong></h3>
<p><strong>Symptom:</strong></p>
<pre><code class="lang-bash">E: Failed to fetch ...
E: Unable to fetch some archives
Timeout was reached
</code></pre>
<p><strong>Why This Happens:</strong></p>
<ul>
<li><p>Using slow/blocked regional mirrors (<a target="_blank" href="http://lk.archive.ubuntu.com">lk.archive.ubuntu.com</a>)</p>
</li>
<li><p>Missing Elastic repository (Filebeat not in standard Ubuntu repos)</p>
</li>
<li><p>Network routing issues for HTTP/HTTPS</p>
</li>
</ul>
<p><strong>Impact:</strong> Cannot install Filebeat, blocking log shipping pipeline.</p>
<p><strong>Root Cause:</strong> Two-part problem:</p>
<ol>
<li><p>Ubuntu mirrors unreachable/slow</p>
</li>
<li><p>Elastic repo not configured</p>
</li>
</ol>
<p><strong>Solution:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Step 1: Fix Ubuntu repositories</span>
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
sudo nano /etc/apt/sources.list

<span class="hljs-comment"># Replace with main Ubuntu mirrors:</span>
deb http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu jammy-security main restricted universe multiverse

<span class="hljs-comment"># Step 2: Add Elastic repository</span>
sudo apt install -y apt-transport-https curl gnupg
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | \
sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
<span class="hljs-built_in">echo</span> <span class="hljs-string">"deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main"</span> | \
sudo tee /etc/apt/sources.list.d/elastic-8.x.list

<span class="hljs-comment"># Step 3: Update and install</span>
sudo apt update
sudo apt install filebeat -y
</code></pre>
<p><strong>Verification:</strong></p>
<pre><code class="lang-bash">filebeat version
<span class="hljs-comment"># Should show: filebeat version 8.x.x</span>
</code></pre>
<p><strong>Why This Matters:</strong></p>
<ul>
<li><p>Filebeat is log shipper - critical component of ELK pipeline</p>
</li>
<li><p>Without it, logs stay local on app-vm</p>
</li>
<li><p>No centralized logging = harder debugging in distributed systems</p>
</li>
</ul>
<hr />
<h3 id="heading-error-3-flask-service-failed-modulenotfounderror"><strong>ERROR #3: Flask Service Failed - ModuleNotFoundError</strong></h3>
<p><strong>Symptom:</strong></p>
<pre><code class="lang-bash">ModuleNotFoundError: No module named <span class="hljs-string">'flask'</span>
systemctl status myapp.service → failed (code=exited, status=1)
</code></pre>
<p><strong>Why This Happens:</strong> Systemd service points to system Python (<code>/usr/bin/python3</code>) instead of virtual environment Python.</p>
<p><strong>How to Identify:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Flask installed in venv:</span>
/home/devops/myapp/venv/bin/python3 -c <span class="hljs-string">"import flask; print('OK')"</span>
<span class="hljs-comment"># Returns: OK</span>

<span class="hljs-comment"># System Python doesn't have Flask:</span>
/usr/bin/python3 -c <span class="hljs-string">"import flask"</span>
<span class="hljs-comment"># Returns: ModuleNotFoundError</span>
</code></pre>
<p><strong>Root Cause:</strong> Virtual environment isolates dependencies. Systemd service must use venv Python, not system Python.</p>
<p><strong>Incorrect Service File:</strong></p>
<pre><code class="lang-ini"><span class="hljs-attr">ExecStart</span>=/usr/bin/python3 /home/devops/myapp/app.py
<span class="hljs-comment"># ❌ Uses system Python → no Flask module</span>
</code></pre>
<p><strong>Correct Service File:</strong></p>
<pre><code class="lang-ini"><span class="hljs-attr">ExecStart</span>=/home/devops/myapp/venv/bin/python3 /home/devops/myapp/app.py
<span class="hljs-comment"># ✅ Uses venv Python → Flask available</span>
</code></pre>
<p><strong>Full Fix:</strong></p>
<pre><code class="lang-bash">sudo systemctl stop myapp.service
sudo nano /etc/systemd/system/myapp.service
<span class="hljs-comment"># Update ExecStart line to use venv/bin/python3</span>
sudo systemctl daemon-reload
sudo systemctl start myapp.service
sudo systemctl status myapp.service
</code></pre>
<p><strong>Verification:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Service should show "active (running)"</span>
sudo systemctl status myapp.service

<span class="hljs-comment"># Test endpoint</span>
curl http://localhost:5000
<span class="hljs-comment"># Should return HTML response</span>
</code></pre>
<p><strong>Why This Matters:</strong></p>
<ul>
<li><p>Common mistake when deploying Python apps</p>
</li>
<li><p>Virtual environments prevent dependency conflicts</p>
</li>
<li><p>Production best practice: isolate app dependencies</p>
</li>
<li><p>Systemd service must match development environment</p>
</li>
</ul>
<p><strong>Prevention:</strong> Always specify full path to venv Python in systemd services.</p>
<h2 id="heading-key-endpoints">🎯 Key Endpoints</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Service</td><td>VM</td><td>URL</td></tr>
</thead>
<tbody>
<tr>
<td>Flask App</td><td>app-vm</td><td><a target="_blank" href="http://192.168.8.50:5000">http://192.168.8.50:5000</a></td></tr>
<tr>
<td>Flask Metrics</td><td>app-vm</td><td><a target="_blank" href="http://192.168.8.50:5000/metrics">http://192.168.8.50:5000/metrics</a></td></tr>
<tr>
<td>Node Exporter</td><td>app-vm</td><td><a target="_blank" href="http://192.168.8.50:9100/metrics">http://192.168.8.50:9100/metrics</a></td></tr>
<tr>
<td>Prometheus</td><td>monitoring-vm</td><td><a target="_blank" href="http://192.168.8.60:9090">http://192.168.8.60:9090</a></td></tr>
<tr>
<td>Grafana</td><td>monitoring-vm</td><td><a target="_blank" href="http://192.168.8.60:3000">http://192.168.8.60:3000</a></td></tr>
<tr>
<td>Elasticsearch</td><td>logging-vm</td><td><a target="_blank" href="http://192.168.8.70:9200">http://192.168.8.70:9200</a></td></tr>
<tr>
<td>Kibana</td><td>logging-vm</td><td><a target="_blank" href="http://192.168.8.70:5601">http://192.168.8.70:5601</a></td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Building a Production-Ready Cloud-Native Microservice with Complete CI/CD Pipeline on AWS EKS]]></title><description><![CDATA[📋 Project Overview
This comprehensive guide walks you through building a production-grade cloud-native microservice using FastAPI, Docker, Kubernetes (AWS EKS), and GitHub Actions CI/CD. You'll learn how to implement enterprise-level DevOps practice...]]></description><link>https://blog.sachindu.me/cloud-native-fastapi-docker-kubernetes-aws-cicd</link><guid isPermaLink="true">https://blog.sachindu.me/cloud-native-fastapi-docker-kubernetes-aws-cicd</guid><category><![CDATA[Devops]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[n8n]]></category><category><![CDATA[n8n Automation]]></category><category><![CDATA[AWS]]></category><category><![CDATA[EKS]]></category><category><![CDATA[ecr]]></category><category><![CDATA[cloud native]]></category><category><![CDATA[autoscaling]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Wed, 24 Dec 2025 08:33:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766664592904/c2511158-2608-485e-bfdf-590cb5d80b2f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-project-overview">📋 Project Overview</h3>
<p>This comprehensive guide walks you through building a <strong>production-grade cloud-native microservice</strong> using FastAPI, Docker, Kubernetes (AWS EKS), and GitHub Actions CI/CD. You'll learn how to implement enterprise-level DevOps practices including automated testing, container orchestration, auto-scaling, health monitoring, and deployment notifications.</p>
<p><strong>What You'll Build:</strong></p>
<ul>
<li><p>Automated CI/CD pipeline using GitHub Actions</p>
</li>
<li><p>Kubernetes deployment on AWS EKS with auto-scaling</p>
</li>
<li><p>Multi-environment setup (dev, staging, production)</p>
</li>
<li><p>Production observability and notifications</p>
</li>
</ul>
<p><strong>Tech Stack:</strong> Python, FastAPI, Docker, Kubernetes, AWS EKS, ECR, GitHub Actions, Prometheus, n8n</p>
<p><strong>GitHub Repository:</strong> <code>https://github.com/sachindumalshan/cloud-native-microservice-pipeline-monitor</code></p>
<hr />
<h2 id="heading-table-of-contents">📑 Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-1-local-development-setup">Phase 1: Local Development Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-2-containerization-with-docker">Phase 2: Containerization with Docker</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-3-version-control--ci-setup">Phase 3: Version Control &amp; CI Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-4-aws-infrastructure-setup">Phase 4: AWS Infrastructure Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-5-kubernetes-deployment">Phase 5: Kubernetes Deployment</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-6-production-reliability-features">Phase 6: Production Reliability Features</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-7-observability--monitoring">Phase 7: Observability &amp; Monitoring</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-8-multi-environment-deployment">Phase 8: Multi-Environment Deployment</a></p>
</li>
<li><p><a class="post-section-overview" href="#phase-9-deployment-notifications">Phase 9: Deployment Notifications</a></p>
</li>
<li><p><a class="post-section-overview" href="#common-errors--solutions">Common Errors &amp; Solutions</a></p>
</li>
<li><p><a class="post-section-overview" href="#conclusion--key-takeaways">Conclusion &amp; Key Takeaways</a></p>
</li>
</ol>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, ensure you have:</p>
<ul>
<li><p>Python 3.12+ installed</p>
</li>
<li><p>Docker Desktop running</p>
</li>
<li><p>AWS Account with appropriate permissions</p>
</li>
<li><p>GitHub account</p>
</li>
<li><p>Basic knowledge of Python, REST APIs, and command line</p>
</li>
<li><p>kubectl and AWS CLI installed</p>
</li>
</ul>
<hr />
<h2 id="heading-phase-1-local-development-setup">Phase 1: Local Development Setup</h2>
<h3 id="heading-step-11-create-project-structure">Step 1.1: Create Project Structure</h3>
<pre><code class="lang-bash">mkdir health_metrics_service
<span class="hljs-built_in">cd</span> health_metrics_service
python3 -m venv venv
<span class="hljs-built_in">source</span> venv/bin/activate
</code></pre>
<h3 id="heading-step-12-install-dependencies">Step 1.2: Install Dependencies</h3>
<pre><code class="lang-bash">pip install fastapi uvicorn pytest
</code></pre>
<h3 id="heading-step-13-create-fastapi-application">Step 1.3: Create FastAPI Application</h3>
<p>Create <code>app.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI
<span class="hljs-keyword">import</span> random, time

app = FastAPI()

<span class="hljs-meta">@app.get("/health")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">health_check</span>():</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"healthy"</span>}

<span class="hljs-meta">@app.get("/metrics")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">metrics</span>():</span>
    cpu_load = random.uniform(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)
    memory_usage = random.uniform(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"cpu"</span>: cpu_load, <span class="hljs-string">"memory"</span>: memory_usage}

<span class="hljs-meta">@app.post("/simulate_load")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">simulate_load</span>(<span class="hljs-params">duration: int = <span class="hljs-number">5</span></span>):</span>
    start = time.time()
    <span class="hljs-keyword">while</span> time.time() - start &lt; duration:
        sum([i**<span class="hljs-number">2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">10000</span>)])
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"load simulated"</span>}
</code></pre>
<h3 id="heading-step-14-test-locally">Step 1.4: Test Locally</h3>
<pre><code class="lang-bash">uvicorn app:app --reload
</code></pre>
<p>Visit <code>http://127.0.0.1:8000/health</code> and <code>http://127.0.0.1:8000/metrics</code> to verify.</p>
<h3 id="heading-step-15-create-unit-tests">Step 1.5: Create Unit Tests</h3>
<p>Create <code>tests/test_health.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi.testclient <span class="hljs-keyword">import</span> TestClient
<span class="hljs-keyword">from</span> app <span class="hljs-keyword">import</span> app

client = TestClient(app)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_health</span>():</span>
    response = client.get(<span class="hljs-string">"/health"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    <span class="hljs-keyword">assert</span> response.json() == {<span class="hljs-string">"status"</span>: <span class="hljs-string">"healthy"</span>}
</code></pre>
<p>Create <code>tests/test_metrics.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi.testclient <span class="hljs-keyword">import</span> TestClient
<span class="hljs-keyword">from</span> app <span class="hljs-keyword">import</span> app

client = TestClient(app)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_metrics</span>():</span>
    response = client.get(<span class="hljs-string">"/metrics"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    data = response.json()
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"cpu"</span> <span class="hljs-keyword">in</span> data
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"memory"</span> <span class="hljs-keyword">in</span> data
</code></pre>
<p>Run tests:</p>
<pre><code class="lang-bash">pytest tests/
</code></pre>
<h3 id="heading-step-16-generate-requirements">Step 1.6: Generate Requirements</h3>
<pre><code class="lang-bash">pip freeze &gt; requirements.txt
</code></pre>
<hr />
<h2 id="heading-phase-2-containerization-with-docker">Phase 2: Containerization with Docker</h2>
<h3 id="heading-step-21-create-dockerfile">Step 2.1: Create Dockerfile</h3>
<p>Create <code>Dockerfile</code>:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.12</span>-slim

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> requirements.txt .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install --no-cache-dir -r requirements.txt</span>

<span class="hljs-keyword">COPY</span><span class="bash"> app.py .</span>

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">8000</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"uvicorn"</span>, <span class="hljs-string">"app:app"</span>, <span class="hljs-string">"--host"</span>, <span class="hljs-string">"0.0.0.0"</span>, <span class="hljs-string">"--port"</span>, <span class="hljs-string">"8000"</span>]</span>
</code></pre>
<h3 id="heading-step-22-build-docker-image">Step 2.2: Build Docker Image</h3>
<pre><code class="lang-bash">docker build -t health-metrics-service:1.0 .
</code></pre>
<h3 id="heading-step-23-run-container">Step 2.3: Run Container</h3>
<pre><code class="lang-bash">docker run -d -p 8000:8000 --name health-metrics health-metrics-service:1.0
</code></pre>
<h3 id="heading-step-24-verify-container">Step 2.4: Verify Container</h3>
<pre><code class="lang-bash">docker ps
curl http://localhost:8000/health
docker logs health-metrics
</code></pre>
<hr />
<h2 id="heading-phase-3-version-control-amp-ci-setup">Phase 3: Version Control &amp; CI Setup</h2>
<h3 id="heading-step-31-initialize-git-repository">Step 3.1: Initialize Git Repository</h3>
<pre><code class="lang-bash">git init
git add .
git commit -m <span class="hljs-string">"Initial commit: FastAPI health &amp; metrics microservice"</span>
</code></pre>
<h3 id="heading-step-32-create-github-repository">Step 3.2: Create GitHub Repository</h3>
<p>Create a new repository on GitHub named <code>cloud-native-microservice-pipeline-monitor</code></p>
<pre><code class="lang-bash">git remote add origin https://github.com/&lt;your-username&gt;/cloud-native-microservice-pipeline-monitor.git
git branch -M main
git push -u origin main
</code></pre>
<h3 id="heading-step-33-setup-github-actions-ci">Step 3.3: Setup GitHub Actions CI</h3>
<p>Create <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">CI</span> <span class="hljs-bullet">-</span> <span class="hljs-string">FastAPI</span> <span class="hljs-string">Tests</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> <span class="hljs-string">main</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span> <span class="hljs-string">main</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">test:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">repository</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Python</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-python@v5</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">python-version:</span> <span class="hljs-string">"3.12"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          pip install --upgrade pip
          pip install -r requirements.txt
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">unit</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">run:</span> 
          <span class="hljs-string">PYTHONPATH=.</span> <span class="hljs-string">pytest</span>
</code></pre>
<h3 id="heading-step-34-add-docker-build-to-ci">Step 3.4: Add Docker Build to CI</h3>
<p>Update <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">CI</span> <span class="hljs-bullet">-</span> <span class="hljs-string">Test</span> <span class="hljs-string">&amp;</span> <span class="hljs-string">Docker</span> <span class="hljs-string">Build</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> <span class="hljs-string">main</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span> <span class="hljs-string">main</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">test-and-build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">repository</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Python</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-python@v5</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">python-version:</span> <span class="hljs-string">"3.12"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          pip install --upgrade pip
          pip install -r requirements.txt
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">unit</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">run:</span> 
          <span class="hljs-string">PYTHONPATH=.</span> <span class="hljs-string">pytest</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">Docker</span> <span class="hljs-string">image</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">docker</span> <span class="hljs-string">build</span> <span class="hljs-string">-t</span> <span class="hljs-string">health-metrics-service:ci</span> <span class="hljs-string">.</span>
</code></pre>
<hr />
<h2 id="heading-phase-4-aws-infrastructure-setup">Phase 4: AWS Infrastructure Setup</h2>
<h3 id="heading-step-41-create-ecr-repository">Step 4.1: Create ECR Repository</h3>
<ol>
<li><p>Navigate to AWS Console → ECR → Repositories</p>
</li>
<li><p>Click "Create repository"</p>
</li>
<li><p>Repository name: <code>health-metrics-service</code></p>
</li>
<li><p>Region: <code>us-east-1</code> (or your preferred region)</p>
</li>
<li><p>Note the repository URI</p>
</li>
</ol>
<h3 id="heading-step-42-create-iam-user-for-github-actions">Step 4.2: Create IAM User for GitHub Actions</h3>
<ol>
<li><p>AWS Console → IAM → Users → Create user</p>
</li>
<li><p>Username: <code>github-actions-ecr</code></p>
</li>
<li><p>Access type: Programmatic access</p>
</li>
<li><p>Attach policy: <code>AmazonEC2ContainerRegistryPowerUser</code></p>
</li>
<li><p>Save Access Key ID and Secret Access Key</p>
</li>
</ol>
<h3 id="heading-step-43-configure-github-secrets">Step 4.3: Configure GitHub Secrets</h3>
<p>Go to GitHub repo → Settings → Secrets and variables → Actions</p>
<p>Add these secrets:</p>
<ul>
<li><p><code>AWS_ACCESS_KEY_ID</code></p>
</li>
<li><p><code>AWS_SECRET_ACCESS_KEY</code></p>
</li>
<li><p><code>AWS_REGION</code> (e.g., <code>us-east-1</code>)</p>
</li>
<li><p><code>ECR_REPOSITORY</code> (e.g., <code>health-metrics-service</code>)</p>
</li>
<li><p><code>AWS_ACCOUNT_ID</code></p>
</li>
</ul>
<h3 id="heading-step-44-update-cicd-to-push-to-ecr">Step 4.4: Update CI/CD to Push to ECR</h3>
<p>Update <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">CI</span> <span class="hljs-bullet">-</span> <span class="hljs-string">Test,</span> <span class="hljs-string">Build</span> <span class="hljs-string">&amp;</span> <span class="hljs-string">Push</span> <span class="hljs-string">to</span> <span class="hljs-string">ECR</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> <span class="hljs-string">main</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span> <span class="hljs-string">main</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build-and-push:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">code</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Python</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-python@v5</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">python-version:</span> <span class="hljs-string">"3.12"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">pip</span> <span class="hljs-string">install</span> <span class="hljs-string">-r</span> <span class="hljs-string">requirements.txt</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">pytest</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Configure</span> <span class="hljs-string">AWS</span> <span class="hljs-string">credentials</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">aws-actions/configure-aws-credentials@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">aws-access-key-id:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_ACCESS_KEY_ID</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">aws-secret-access-key:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_SECRET_ACCESS_KEY</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">aws-region:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_REGION</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Login</span> <span class="hljs-string">to</span> <span class="hljs-string">Amazon</span> <span class="hljs-string">ECR</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">aws-actions/amazon-ecr-login@v2</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">Docker</span> <span class="hljs-string">image</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">ECR_REGISTRY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_ACCOUNT_ID</span> <span class="hljs-string">}}.dkr.ecr.${{</span> <span class="hljs-string">secrets.AWS_REGION</span> <span class="hljs-string">}}.amazonaws.com</span>
          <span class="hljs-attr">ECR_REPOSITORY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.ECR_REPOSITORY</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">IMAGE_TAG:</span> <span class="hljs-string">latest</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG</span>
</code></pre>
<h3 id="heading-step-45-create-eks-cluster">Step 4.5: Create EKS Cluster</h3>
<ol>
<li><p>AWS Console → EKS → Clusters → Create cluster</p>
</li>
<li><p>Cluster name: <code>health-metrics-cluster</code></p>
</li>
<li><p>Kubernetes version: Latest stable</p>
</li>
<li><p>Configure networking (VPC, subnets)</p>
</li>
<li><p>Wait ~10 minutes for cluster creation</p>
</li>
</ol>
<h3 id="heading-step-46-configure-kubectl-access">Step 4.6: Configure kubectl Access</h3>
<pre><code class="lang-bash">aws eks update-kubeconfig --name health-metrics-cluster --region us-east-1
</code></pre>
<h3 id="heading-step-47-create-eks-access-entry-for-github-actions">Step 4.7: Create EKS Access Entry for GitHub Actions</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Create access entry</span>
aws eks create-access-entry \
  --cluster-name health-metrics-cluster \
  --region us-east-1 \
  --principal-arn arn:aws:iam::&lt;AWS_ACCOUNT_ID&gt;:user/github-actions-ecr \
  --<span class="hljs-built_in">type</span> STANDARD

<span class="hljs-comment"># Grant admin permissions</span>
aws eks associate-access-policy \
  --cluster-name health-metrics-cluster \
  --region us-east-1 \
  --principal-arn arn:aws:iam::&lt;AWS_ACCOUNT_ID&gt;:user/github-actions-ecr \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope <span class="hljs-built_in">type</span>=cluster
</code></pre>
<hr />
<h2 id="heading-phase-5-kubernetes-deployment">Phase 5: Kubernetes Deployment</h2>
<h3 id="heading-step-51-create-kubernetes-manifests">Step 5.1: Create Kubernetes Manifests</h3>
<p>Create <code>k8s/deployment.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">health-metrics-deployment</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">health-metrics</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">health-metrics</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">health-metrics-container</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">&lt;AWS_ACCOUNT_ID&gt;.dkr.ecr.&lt;REGION&gt;.amazonaws.com/health-metrics-service:latest</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8000</span>
</code></pre>
<p>Create <code>k8s/service.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">health-metrics-service</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">LoadBalancer</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">health-metrics</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">targetPort:</span> <span class="hljs-number">8000</span>
</code></pre>
<h3 id="heading-step-52-add-eks-deployment-to-cicd">Step 5.2: Add EKS Deployment to CI/CD</h3>
<p>Add to <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">kubeconfig</span> <span class="hljs-string">for</span> <span class="hljs-string">EKS</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          aws eks update-kubeconfig \
            --region ${{ secrets.AWS_REGION }} \
            --name health-metrics-cluster
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">EKS</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-string">k8s/</span>
</code></pre>
<h3 id="heading-step-53-add-github-secret-for-eks">Step 5.3: Add GitHub Secret for EKS</h3>
<p>Add <code>EKS_CLUSTER_NAME: health-metrics-cluster</code> to GitHub Secrets</p>
<hr />
<h2 id="heading-phase-6-production-reliability-features">Phase 6: Production Reliability Features</h2>
<h3 id="heading-step-61-add-health-probes">Step 6.1: Add Health Probes</h3>
<p>Update <code>k8s/deployment.yaml</code> container spec:</p>
<pre><code class="lang-yaml">        <span class="hljs-attr">livenessProbe:</span>
          <span class="hljs-attr">httpGet:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">/health</span>
            <span class="hljs-attr">port:</span> <span class="hljs-number">8000</span>
          <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">10</span>
          <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">10</span>

        <span class="hljs-attr">readinessProbe:</span>
          <span class="hljs-attr">httpGet:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">/health</span>
            <span class="hljs-attr">port:</span> <span class="hljs-number">8000</span>
          <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">5</span>
          <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">10</span>
</code></pre>
<h3 id="heading-step-62-configure-resource-limits">Step 6.2: Configure Resource Limits</h3>
<p>Add to container spec in <code>k8s/deployment.yaml</code>:</p>
<pre><code class="lang-yaml">        <span class="hljs-attr">resources:</span>
          <span class="hljs-attr">requests:</span>
            <span class="hljs-attr">cpu:</span> <span class="hljs-string">"100m"</span>
            <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
          <span class="hljs-attr">limits:</span>
            <span class="hljs-attr">cpu:</span> <span class="hljs-string">"500m"</span>
            <span class="hljs-attr">memory:</span> <span class="hljs-string">"256Mi"</span>
</code></pre>
<h3 id="heading-step-63-setup-horizontal-pod-autoscaler">Step 6.3: Setup Horizontal Pod Autoscaler</h3>
<p>Create <code>k8s/hpa.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">autoscaling/v2</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">HorizontalPodAutoscaler</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">health-metrics-hpa</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">scaleTargetRef:</span>
    <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">health-metrics-deployment</span>
  <span class="hljs-attr">minReplicas:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">maxReplicas:</span> <span class="hljs-number">5</span>
  <span class="hljs-attr">metrics:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">Resource</span>
    <span class="hljs-attr">resource:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">cpu</span>
      <span class="hljs-attr">target:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">Utilization</span>
        <span class="hljs-attr">averageUtilization:</span> <span class="hljs-number">70</span>
</code></pre>
<p>Apply:</p>
<pre><code class="lang-bash">kubectl apply -f k8s/hpa.yaml
</code></pre>
<h3 id="heading-step-64-configure-rolling-updates">Step 6.4: Configure Rolling Updates</h3>
<p>Add to <code>k8s/deployment.yaml</code> spec:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">strategy:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">RollingUpdate</span>
    <span class="hljs-attr">rollingUpdate:</span>
      <span class="hljs-attr">maxSurge:</span> <span class="hljs-number">1</span>
      <span class="hljs-attr">maxUnavailable:</span> <span class="hljs-number">0</span>
</code></pre>
<hr />
<h2 id="heading-phase-7-observability-amp-monitoring">Phase 7: Observability &amp; Monitoring</h2>
<h3 id="heading-step-71-implement-prometheus-metrics">Step 7.1: Implement Prometheus Metrics</h3>
<p>Update <code>requirements.txt</code>:</p>
<pre><code class="lang-bash">fastapi
uvicorn
prometheus-client
pytest
</code></pre>
<p>Update <code>app.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, Response
<span class="hljs-keyword">from</span> prometheus_client <span class="hljs-keyword">import</span> Counter, Gauge, generate_latest
<span class="hljs-keyword">import</span> random, time

app = FastAPI()

<span class="hljs-comment"># Prometheus metrics</span>
REQUEST_COUNT = Counter(<span class="hljs-string">'app_requests_total'</span>, <span class="hljs-string">'Total API requests'</span>)
CPU_USAGE = Gauge(<span class="hljs-string">'cpu_usage_percent'</span>, <span class="hljs-string">'CPU usage percent'</span>)
MEMORY_USAGE = Gauge(<span class="hljs-string">'memory_usage_percent'</span>, <span class="hljs-string">'Memory usage percent'</span>)

<span class="hljs-meta">@app.get("/health")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">health_check</span>():</span>
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"healthy"</span>}

<span class="hljs-meta">@app.get("/metrics")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">metrics_endpoint</span>():</span>
    CPU_USAGE.set(random.uniform(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>))
    MEMORY_USAGE.set(random.uniform(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>))
    REQUEST_COUNT.inc()
    <span class="hljs-keyword">return</span> Response(generate_latest(), media_type=<span class="hljs-string">"text/plain"</span>)

<span class="hljs-meta">@app.post("/simulate_load")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">simulate_load</span>(<span class="hljs-params">duration: int = <span class="hljs-number">5</span></span>):</span>
    start = time.time()
    <span class="hljs-keyword">while</span> time.time() - start &lt; duration:
        sum([i**<span class="hljs-number">2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">10000</span>)])
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"load simulated"</span>}
</code></pre>
<h3 id="heading-step-72-update-test-for-prometheus-format">Step 7.2: Update Test for Prometheus Format</h3>
<p>Update <code>tests/test_metrics.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> fastapi.testclient <span class="hljs-keyword">import</span> TestClient
<span class="hljs-keyword">from</span> app <span class="hljs-keyword">import</span> app

client = TestClient(app)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_metrics</span>():</span>
    response = client.get(<span class="hljs-string">"/metrics"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    content = response.text
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"cpu_usage_percent"</span> <span class="hljs-keyword">in</span> content
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"memory_usage_percent"</span> <span class="hljs-keyword">in</span> content
</code></pre>
<hr />
<h2 id="heading-phase-8-multi-environment-deployment">Phase 8: Multi-Environment Deployment</h2>
<h3 id="heading-step-81-update-cicd-for-multiple-namespaces">Step 8.1: Update CI/CD for Multiple Namespaces</h3>
<p>Update <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">namespace</span> <span class="hljs-string">based</span> <span class="hljs-string">on</span> <span class="hljs-string">branch</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          if [ "${GITHUB_REF_NAME}" == "development" ]; then
            NAMESPACE="dev"
          elif [ "${GITHUB_REF_NAME}" == "staging" ]; then
            NAMESPACE="staging"
          elif [ "${GITHUB_REF_NAME}" == "main" ]; then
            NAMESPACE="prod"
          else
            echo "Unknown branch, skipping deployment"
            exit 0
          fi
          echo "Namespace=$NAMESPACE" &gt;&gt; $GITHUB_ENV
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Create</span> <span class="hljs-string">namespace</span> <span class="hljs-string">if</span> <span class="hljs-string">missing</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          kubectl get namespace ${{ env.Namespace }} || kubectl create namespace ${{ env.Namespace }}
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">EKS</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "Deploying to ${{ env.Namespace }} namespace..."
          kubectl apply -f k8s/ -n ${{ env.Namespace }}
          kubectl get all -n ${{ env.Namespace }}</span>
</code></pre>
<hr />
<h2 id="heading-phase-9-deployment-notifications">Phase 9: Deployment Notifications</h2>
<h3 id="heading-step-91-setup-n8n-webhook">Step 9.1: Setup n8n Webhook</h3>
<ol>
<li><p>Create n8n workflow with Webhook node</p>
</li>
<li><p>Add Slack/Email notification node</p>
</li>
<li><p>Configure message template</p>
</li>
<li><p>Note webhook URL</p>
</li>
</ol>
<h3 id="heading-step-92-add-notification-step-to-cicd">Step 9.2: Add Notification Step to CI/CD</h3>
<p>Add to <code>.github/workflows/ci.yml</code>:</p>
<pre><code class="lang-yaml">      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Notify</span> <span class="hljs-string">n8n</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">always()</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          STATUS="success"
          if [ "${{ job.status }}" != "success" ]; then
            STATUS="failure"
          fi
</span>
          <span class="hljs-string">curl</span> <span class="hljs-string">-X</span> <span class="hljs-string">POST</span> <span class="hljs-string">https://&lt;n8n-webhook-url&gt;/deployments</span> <span class="hljs-string">\</span>
            <span class="hljs-string">-H</span> <span class="hljs-string">'Content-Type: application/json'</span> <span class="hljs-string">\</span>
            <span class="hljs-string">-d</span> <span class="hljs-string">'{
                  "service": "health-metrics-service",
                  "namespace": "$<span class="hljs-template-variable">{{ env.Namespace }}</span>",
                  "status": "'</span><span class="hljs-string">"$STATUS"</span><span class="hljs-string">'"
                }'</span>
</code></pre>
<hr />
<h2 id="heading-common-errors-amp-solutions">Common Errors &amp; Solutions</h2>
<h3 id="heading-error-1-eks-authentication-failure">Error 1: EKS Authentication Failure</h3>
<p><strong>Symptoms:</strong></p>
<pre><code class="lang-bash">error: You must be logged <span class="hljs-keyword">in</span> to the server (Unauthorized)
couldn<span class="hljs-string">'t get current server API group list</span>
</code></pre>
<p><strong>Root Cause:</strong> Missing EKS access entry for GitHub Actions IAM user</p>
<p><strong>Solution:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create access entry</span>
aws eks create-access-entry \
  --cluster-name health-metrics-cluster \
  --region us-east-1 \
  --principal-arn arn:aws:iam::&lt;ACCOUNT_ID&gt;:user/github-actions-ecr \
  --<span class="hljs-built_in">type</span> STANDARD

<span class="hljs-comment"># Grant permissions</span>
aws eks associate-access-policy \
  --cluster-name health-metrics-cluster \
  --region us-east-1 \
  --principal-arn arn:aws:iam::&lt;ACCOUNT_ID&gt;:user/github-actions-ecr \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope <span class="hljs-built_in">type</span>=cluster
</code></pre>
<h3 id="heading-error-2-stale-kubeconfig">Error 2: Stale Kubeconfig</h3>
<p><strong>Symptoms:</strong></p>
<pre><code class="lang-bash">Unable to connect to the server: dial tcp: lookup &lt;endpoint&gt; on 127.0.0.53:53: no such host
</code></pre>
<p><strong>Solution:</strong></p>
<pre><code class="lang-bash">rm -f ~/.kube/config
aws eks update-kubeconfig --name health-metrics-cluster --region us-east-1
kubectl get nodes
</code></pre>
<h3 id="heading-error-3-loadbalancer-pending">Error 3: LoadBalancer Pending</h3>
<p><strong>Symptoms:</strong></p>
<pre><code class="lang-bash">EXTERNAL-IP: &lt;pending&gt;
</code></pre>
<p><strong>Root Cause:</strong> AWS Load Balancer Controller not installed</p>
<p><strong>Quick Fix - Use NodePort:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Switch to NodePort</span>
kubectl patch service health-metrics-service -n prod -p <span class="hljs-string">'{"spec":{"type":"NodePort"}}'</span>

<span class="hljs-comment"># Get NodePort</span>
kubectl get service health-metrics-service -n prod

<span class="hljs-comment"># Open security group</span>
aws ec2 authorize-security-group-ingress \
  --group-id &lt;node-sg&gt; \
  --protocol tcp \
  --port &lt;node-port&gt; \
  --cidr 0.0.0.0/0
</code></pre>
<p><strong>Proper Solution - Install AWS Load Balancer Controller:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Download IAM policy</span>
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.7.1/docs/install/iam_policy.json

<span class="hljs-comment"># Create policy</span>
aws iam create-policy \
  --policy-name AWSLoadBalancerControllerIAMPolicy \
  --policy-document file://iam_policy.json

<span class="hljs-comment"># Associate OIDC provider</span>
eksctl utils associate-iam-oidc-provider \
  --region us-east-1 \
  --cluster health-metrics-cluster \
  --approve

<span class="hljs-comment"># Create service account</span>
eksctl create iamserviceaccount \
  --cluster=health-metrics-cluster \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --role-name AmazonEKSLoadBalancerControllerRole \
  --attach-policy-arn=arn:aws:iam::&lt;ACCOUNT_ID&gt;:policy/AWSLoadBalancerControllerIAMPolicy \
  --approve \
  --region=us-east-1

<span class="hljs-comment"># Install controller via Helm</span>
helm repo add eks https://aws.github.io/eks-charts
helm repo update

VPC_ID=$(aws eks describe-cluster \
  --name health-metrics-cluster \
  --region us-east-1 \
  --query <span class="hljs-string">"cluster.resourcesVpcConfig.vpcId"</span> \
  --output text)

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --<span class="hljs-built_in">set</span> clusterName=health-metrics-cluster \
  --<span class="hljs-built_in">set</span> serviceAccount.create=<span class="hljs-literal">false</span> \
  --<span class="hljs-built_in">set</span> serviceAccount.name=aws-load-balancer-controller \
  --<span class="hljs-built_in">set</span> region=us-east-1 \
  --<span class="hljs-built_in">set</span> vpcId=<span class="hljs-variable">$VPC_ID</span>
</code></pre>
<h3 id="heading-error-4-jsondecodeerror-in-tests">Error 4: JSONDecodeError in Tests</h3>
<p><strong>Symptoms:</strong></p>
<pre><code class="lang-bash">json.decoder.JSONDecodeError: Expecting value: line 1 column 1
</code></pre>
<p><strong>Root Cause:</strong> Prometheus metrics return text format, not JSON</p>
<p><strong>Solution:</strong></p>
<p>Update <code>tests/test_metrics.py</code>:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_metrics</span>():</span>
    response = client.get(<span class="hljs-string">"/metrics"</span>)
    <span class="hljs-keyword">assert</span> response.status_code == <span class="hljs-number">200</span>
    content = response.text
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"cpu_usage_percent"</span> <span class="hljs-keyword">in</span> content
    <span class="hljs-keyword">assert</span> <span class="hljs-string">"memory_usage_percent"</span> <span class="hljs-keyword">in</span> content
</code></pre>
<h3 id="heading-error-5-oidc-provider-missing">Error 5: OIDC Provider Missing</h3>
<p><strong>Symptoms:</strong></p>
<pre><code class="lang-bash">Error: no IAM OIDC provider associated with cluster
</code></pre>
<p><strong>Solution:</strong></p>
<pre><code class="lang-bash">eksctl utils associate-iam-oidc-provider \
  --region us-east-1 \
  --cluster health-metrics-cluster \
  --approve
</code></pre>
<h3 id="heading-error-6-pytest-cannot-find-module-modulenotfounderror">Error 6: pytest Cannot Find Module (ModuleNotFoundError)</h3>
<p><strong>Symptoms:</strong></p>
<pre><code class="lang-bash">ModuleNotFoundError: No module named <span class="hljs-string">'app'</span>
ImportError: cannot import name <span class="hljs-string">'app'</span> from <span class="hljs-string">'app'</span>
</code></pre>
<p><strong>Root Cause:</strong> Python cannot locate your module because the project root isn't in the PYTHONPATH</p>
<p><strong>Solutions:</strong></p>
<p><strong>Option 1: Quick Fix (Temporary)</strong></p>
<pre><code class="lang-bash">PYTHONPATH=. pytest tests/
</code></pre>
<p><strong>Option 2: Set PYTHONPATH in Virtual Environment (Persistent)</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add to your virtual environment activation</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">'export PYTHONPATH="${PYTHONPATH}:$(pwd)"'</span> &gt;&gt; venv/bin/activate

<span class="hljs-comment"># Reactivate virtual environment</span>
deactivate
<span class="hljs-built_in">source</span> venv/bin/activate

<span class="hljs-comment"># Now pytest works normally</span>
pytest tests/
</code></pre>
<p><strong>Option 3: Create pytest Configuration File (Recommended)</strong></p>
<p>Create <code>pytest.ini</code> in project root:</p>
<pre><code class="lang-ini"><span class="hljs-section">[pytest]</span>
<span class="hljs-attr">pythonpath</span> = .
<span class="hljs-attr">testpaths</span> = tests
</code></pre>
<p>Or create <code>pyproject.toml</code>:</p>
<pre><code class="lang-toml"><span class="hljs-section">[tool.pytest.ini_options]</span>
<span class="hljs-attr">pythonpath</span> = [<span class="hljs-string">"."</span>]
<span class="hljs-attr">testpaths</span> = [<span class="hljs-string">"tests"</span>]
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<pre><code class="lang-bash">Developer Push to GitHub
         ↓
GitHub Actions CI/CD
         ↓
Run Tests (pytest)
         ↓
Build Docker Image
         ↓
Push to AWS ECR
         ↓
Deploy to EKS (kubectl)
         ↓
Kubernetes Cluster
  ├─ Deployment (2-5 replicas)
  ├─ HPA (auto-scaling)
  ├─ Service (LoadBalancer)
  └─ Health Probes
         ↓
n8n Notification (Slack/Email)
</code></pre>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://fastapi.tiangolo.com/">FastAPI Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/">Kubernetes Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://aws.github.io/aws-eks-best-practices/">AWS EKS Best Practices</a></p>
</li>
<li><p><a target="_blank" href="https://docs.github.com/en/actions">GitHub Actions Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://prometheus.io/docs/">Prometheus Documentation</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Run AWS Services Locally on Your PC for Free with LocalStack]]></title><description><![CDATA[While exploring tools for local cloud development, LocalStack was a powerful tool that simulates AWS services directly on your computer. It is used by developers to build and test AWS-based applications without accessing the real AWS cloud, saving ti...]]></description><link>https://blog.sachindu.me/run-aws-services-locally-on-pc-free-with-localstack</link><guid isPermaLink="true">https://blog.sachindu.me/run-aws-services-locally-on-pc-free-with-localstack</guid><category><![CDATA[Cloud Computing]]></category><category><![CDATA[AWS]]></category><category><![CDATA[localstack]]></category><category><![CDATA[aws-services]]></category><category><![CDATA[free]]></category><category><![CDATA[localhost]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Thu, 13 Nov 2025 17:17:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763031177961/ada81488-db4e-4fed-b221-ee922aeefda6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While exploring tools for local cloud development, <strong>LocalStack</strong> was a powerful tool that <strong>simulates AWS services</strong> directly on your computer. It <strong>is used</strong> by developers to <strong>build and test AWS-based applications</strong> without accessing the real AWS cloud, <strong>saving time and money</strong>.</p>
<h3 id="heading-1-what-is-localstack">1. What is LocalStack?</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763031385191/6a1b7549-0729-40f6-a110-157d4385e709.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>LocalStack</strong> is a fully functional local cloud stack that emulates many AWS services like S3, Lambda, DynamoDB, SQS, SNS, and more.</p>
<p>It helps developers:</p>
<ul>
<li><p>Develop and test AWS applications offline at no cost</p>
</li>
<li><p>Avoid accidental changes to production AWS environments</p>
</li>
</ul>
<p>You can use LocalStack with tools like the AWS CLI, SDKs, and Terraform just like real AWS!</p>
<hr />
<h3 id="heading-2-getting-a-license">2. Getting a License</h3>
<p>LocalStack has both a <strong>Free (Community)</strong> version and a <strong>Pro</strong> version with extra features.</p>
<p><strong>Student tip:</strong></p>
<ul>
<li><p>With the <strong>GitHub Student Developer Pack</strong>, you can get a <strong>free LocalStack Pro license key</strong></p>
</li>
<li><p>Otherwise, sign up for a <strong>14-day free trial</strong> at <a target="_blank" href="https://localstack.cloud/">LocalStack website</a></p>
</li>
</ul>
<p>After signing up, you'll receive a license key to activate LocalStack Pro.</p>
<hr />
<h3 id="heading-3-setting-up-localstack-on-your-pc">3. Setting Up LocalStack on Your PC</h3>
<p>There are two main ways to install and run LocalStack:</p>
<h4 id="heading-option-1-install-directly-on-your-machine">Option 1: Install Directly on Your Machine</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. Install Python (3.8 or higher) and pip</span>
<span class="hljs-comment"># 2. Install LocalStack using pip:</span>
pip install localstack

<span class="hljs-comment"># 3. Set your token (if you have one):</span>
localstack auth set-token &lt;token&gt;

<span class="hljs-comment"># 4. Start LocalStack:</span>
localstack start
</code></pre>
<h4 id="heading-option-2-run-using-docker-recommended">Option 2: Run Using Docker (Recommended)</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. Make sure Docker is installed and running</span>
<span class="hljs-comment"># 2. Pull the LocalStack Docker image:</span>
docker pull localstack/localstack

<span class="hljs-comment"># 3. Run LocalStack in a container:</span>
docker run -d -p 4566:4566 -p 4571:4571 localstack/localstack
</code></pre>
<p>LocalStack will start running on port <strong>4566</strong> (the main gateway for AWS service emulation).</p>
<hr />
<h3 id="heading-4-using-the-localstack-dashboard">4. Using the LocalStack Dashboard</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763031529260/0ef2cfec-065a-4aea-b8eb-28ec823af36d.jpeg" alt class="image--center mx-auto" /></p>
<p>Once LocalStack is running, open your browser and go to:</p>
<p>👉 <a target="_blank" href="http://localhost:4566/">http://localhost:4566</a></p>
<p>You'll see the <strong>LocalStack Web Dashboard</strong>, which shows:</p>
<ul>
<li><p>Running status of your LocalStack instance</p>
</li>
<li><p>List of available AWS services (S3, DynamoDB, etc.)</p>
</li>
<li><p>Logs and configuration options</p>
</li>
</ul>
<p>And also activate your license key from the dashboard.</p>
<hr />
<h3 id="heading-5-accessing-localstack-using-aws-cli">5. Accessing LocalStack Using AWS CLI</h3>
<p>LocalStack works seamlessly with the AWS CLI.</p>
<p><strong>Step 1: Configure AWS CLI</strong> (use dummy credentials):</p>
<pre><code class="lang-bash">aws configure

<span class="hljs-comment"># AWS Access Key ID [None]: test</span>
<span class="hljs-comment"># AWS Secret Access Key [None]: test</span>
<span class="hljs-comment"># Default region name [us-east-1]: us-east-1</span>
<span class="hljs-comment"># Default output format [None]: JSON</span>
</code></pre>
<p>Enter any values for Access Key, Secret Key, and Region (e.g., <code>us-east-1</code>).</p>
<p><strong>Step 2: Point to LocalStack</strong></p>
<p>Always add <code>--endpoint-url=http://localhost:4566</code> to your AWS commands.</p>
<p>Example:</p>
<pre><code class="lang-bash">aws --endpoint-url=http://localhost:4566 s3 ls
</code></pre>
<hr />
<h3 id="heading-example-create-an-s3-bucket-in-localstack">📦 Example: Create an S3 Bucket in LocalStack</h3>
<p>Let's create an S3 bucket locally step by step.</p>
<h4 id="heading-step-1-make-sure-localstack-is-running">Step 1: Make Sure LocalStack is Running</h4>
<p>Verify LocalStack is up (using Docker or direct installation).</p>
<h4 id="heading-step-2-create-a-bucket">Step 2: Create a Bucket</h4>
<pre><code class="lang-bash">aws --endpoint-url=http://localhost:4566 s3 mb s3://my-local-bucket
</code></pre>
<h4 id="heading-step-3-verify-bucket-creation">Step 3: Verify Bucket Creation</h4>
<pre><code class="lang-bash">aws --endpoint-url=http://localhost:4566 s3 ls

<span class="hljs-comment"># 2025-11-13 22:38:01 my-local-bucket</span>
</code></pre>
<p>You should see your new bucket listed!</p>
<h4 id="heading-step-4-upload-a-file">Step 4: Upload a File</h4>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"Hello LocalStack!"</span> &gt; test.txt
aws --endpoint-url=http://localhost:4566 s3 cp test.txt s3://my-local-bucket/
</code></pre>
<h4 id="heading-step-5-list-files-in-the-bucket">Step 5: List Files in the Bucket</h4>
<pre><code class="lang-bash">aws --endpoint-url=http://localhost:4566 s3 ls s3://my-local-bucket/

<span class="hljs-comment"># 2025-11-13 22:38:52         18 test.txt</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763054012425/7d5f530e-2bd0-4a63-845e-3071d365bac1.jpeg" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-conclusion">✅ Conclusion</h3>
<p>LocalStack is a fantastic tool for developers who want to:</p>
<ul>
<li><p>Experiment with AWS locally</p>
</li>
<li><p>Avoid unnecessary cloud costs</p>
</li>
<li><p>Test automation pipelines or serverless applications safely</p>
</li>
</ul>
<p>When learning AWS, building a local test environment, or developing CI/CD workflows, <strong>LocalStack makes local cloud development easy and efficient</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Creating a Hybrid Cloud: Integrate AWS and Proxmox Homelab Using Tailscale]]></title><description><![CDATA[This guide walks you through setting up a clean, reliable hybrid cloud between AWS and a Proxmox homelab using Tailscale. It covers the end‑to‑end network path, on‑prem provisioning (Proxmox + LXC), routing/NAT, verification, and a comprehensive trou...]]></description><link>https://blog.sachindu.me/create-hybrid-cloud-integrate-aws-proxmox-homelab-tailscale</link><guid isPermaLink="true">https://blog.sachindu.me/create-hybrid-cloud-integrate-aws-proxmox-homelab-tailscale</guid><category><![CDATA[AWS]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[containers]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[server]]></category><category><![CDATA[Hybrid Cloud]]></category><category><![CDATA[Site Reliability Engineering]]></category><category><![CDATA[Platform Engineering ]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Sun, 09 Nov 2025 17:08:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762713118055/ea5a108a-ba95-4fa4-a3a9-1a5a297275e1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This guide walks you through setting up a clean, reliable hybrid cloud between AWS and a Proxmox homelab using Tailscale. It covers the end‑to‑end network path, on‑prem provisioning (Proxmox + LXC), routing/NAT, verification, and a comprehensive troubleshooting section for network connectivity and database integration.</p>
<p><code>Use this as the foundation. For the application deployment (frontend, backend, DB schema, monitoring), continue in Article 2 in soon.</code></p>
<h2 id="heading-table-of-contents">Table of contents</h2>
<ul>
<li><p>Install Proxmox on your homelab PC</p>
</li>
<li><p>Access the Proxmox dashboard from another PC</p>
</li>
<li><p>Proxmox components at a glance</p>
</li>
<li><p>Overview and architecture (basic flow)</p>
</li>
<li><p>Create LXC container (ct-db)</p>
</li>
<li><p>Create an AWS EC2 instance</p>
</li>
<li><p>Tailscale setup (Proxmox host + EC2)</p>
</li>
<li><p>Enable forwarding and NAT on Proxmox</p>
</li>
<li><p>Verify EC2-to-container connectivity</p>
</li>
<li><p>Troubleshooting: Network connectivity &amp; DB integration</p>
</li>
</ul>
<hr />
<h2 id="heading-install-proxmox-on-your-homelab-pc">Install Proxmox on your homelab PC</h2>
<ol>
<li><p>Download Proxmox VE ISO</p>
<ul>
<li><a target="_blank" href="https://www.proxmox.com/en/downloads">https://www.proxmox.com/en/downloads</a></li>
</ul>
</li>
<li><p>Create a bootable USB from the ISO (Rufus, balenaEtcher, or dd)</p>
</li>
<li><p>Boot your homelab PC from USB and follow the Proxmox installer</p>
</li>
<li><p>During network setup:</p>
<ul>
<li><p>Set a static IP in your LAN (e.g., 192.168.8.100)</p>
</li>
<li><p>Gateway = your router (e.g., 192.168.8.1)</p>
</li>
<li><p>DNS = your router or 8.8.8.8</p>
</li>
</ul>
</li>
<li><p>Finish install and reboot. On the host console you’ll see something like:</p>
<ul>
<li><p>Management URL: https://&lt;proxmox-ip&gt;:8006/</p>
</li>
<li><p>Login with the credentials you created</p>
</li>
</ul>
</li>
</ol>
<p>Tip: The first login may show a subscription warning; you can still proceed without a subscription.</p>
<hr />
<h2 id="heading-access-the-proxmox-dashboard-from-another-pc">Access the Proxmox dashboard from another PC</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762711730343/279cb844-8d74-4c12-8a3d-4ef7060bc93c.png" alt class="image--center mx-auto" /></p>
<p>From another PC in the same network:</p>
<ul>
<li><p>Open a browser and go to: https://&lt;proxmox_ip&gt;:8006/</p>
</li>
<li><p>Accept the self-signed certificate warning</p>
</li>
<li><p>Login as the user created during install</p>
</li>
</ul>
<p>Now have access to the Proxmox web UI to create containers and manage networking, storage, and backups.</p>
<hr />
<h2 id="heading-proxmox-components-at-a-glance">Proxmox components at a glance</h2>
<ul>
<li><p>Node (proxmox): the physical host that runs everything (VMs/containers)</p>
</li>
<li><p>LXC containers (CTs): lightweight OS environments sharing the host kernel; fast and efficient for services like DBs and monitoring</p>
</li>
<li><p>SDN: optional virtual networking features (VLANs, overlays); can be ignored initially</p>
</li>
<li><p>Storage:</p>
<ul>
<li><p>local (/var/lib/vz): ISOs, templates, backups</p>
</li>
<li><p>local-lvm: VM/CT disks (faster for root disks)</p>
</li>
</ul>
</li>
</ul>
<p>Conceptual layout:</p>
<pre><code class="lang-plaintext">Datacenter
└── Node: proxmox (your physical server)
      ├── Containers:
      │     ├── 101 (ct-db)
      ├── Networks:
      │     └── vmbr0 (LAN bridge)
      └── Storages:
              ├── local (ISO/backups)
              └── local-lvm (VM/CT disks)
</code></pre>
<hr />
<h2 id="heading-overview-and-architecture-basic-flow">Overview and architecture (basic flow)</h2>
<p>Minimal hybrid layout: one EC2 instance in AWS talks privately (via Tailscale) to a single LXC container (ct-db) on your Proxmox host.</p>
<pre><code class="lang-plaintext">   🌩️ AWS Cloud
┌─────────────────┐
│   EC2 Instance  │  (App / Test Client)
└─────────┬───────┘
          │
    (Tailscale VPN)
          │
┌─────────┴────────-┐
│   Proxmox Host    │
│  ┌─────────────┐  │
│  │ LXC: ct-db  │  │  (MariaDB / PostgreSQL)
│  └─────────────┘  │
└───────────────────┘
</code></pre>
<p>Goal: Secure, low-latency private connectivity between cloud and homelab for development and experimentation.</p>
<hr />
<h2 id="heading-create-lxc-container-ct-db">Create LXC container (ct-db)</h2>
<p>Create a single lightweight LXC container for the database.</p>
<p>Suggested specs:</p>
<ul>
<li>ct-db: 2 vCPU, 2–3 GB RAM, Disk 10 GB, static IP (e.g., 192.168.8.101/24), gateway 192.168.8.1</li>
</ul>
<p>Steps:</p>
<ol>
<li><p>Datacenter → proxmox → local (storage) → Templates → download Ubuntu template</p>
</li>
<li><p>Create CT → set Hostname ct-db, assign static IP 192.168.8.101/24, gateway 192.168.8.1, bridge vmbr0</p>
</li>
<li><p>Resources: 2 cores, 2048 MB RAM, optional 512 MB swap</p>
</li>
<li><p>Finish → Start container → Console → verify network: <code>ping 8.8.8.8 -c 5</code></p>
</li>
<li><p>Enable “Start at boot” (ct-db → Options → Start at boot → Enable)</p>
</li>
</ol>
<hr />
<h2 id="heading-create-an-aws-ec2-instance">Create an AWS EC2 instance</h2>
<ol>
<li><p>Create a minimal EC2 instance (for testing)</p>
<ul>
<li><p>AMI: Amazon Linux 2023 (or Ubuntu 22.04 LTS)</p>
</li>
<li><p>Type: t2.nano or t2.micro</p>
</li>
<li><p>Network: place in your VPC (public or private subnet is fine for testing)</p>
</li>
<li><p>Security Group: allow SSH from your IP (port 22)</p>
</li>
</ul>
</li>
<li><p>Connect via SSH using your key pair</p>
</li>
</ol>
<hr />
<h2 id="heading-tailscale-setup-proxmox-host-ec2">Tailscale setup (Proxmox host + EC2)</h2>
<p>Install and authenticate Tailscale on both the Proxmox host and the EC2 instance using the SAME account.</p>
<p>On Proxmox host (advertise the LXC subnet):</p>
<pre><code class="lang-plaintext">curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --advertise-routes=192.168.8.0/24
</code></pre>
<p>On EC2 (accept advertised routes):</p>
<pre><code class="lang-plaintext">curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-routes
</code></pre>
<p>Validation:</p>
<pre><code class="lang-plaintext">tailscale status          # both peers listed
ip route get 192.168.8.101  # should show dev tailscale0
</code></pre>
<p>If the route does not appear, re-run EC2 command with <code>--accept-routes</code> or check ACLs in the Tailscale admin console.</p>
<hr />
<h2 id="heading-enable-forwarding-and-nat-on-proxmox">Enable forwarding and NAT on Proxmox</h2>
<p>By default, Tailscale traffic terminates at the Proxmox host. Enable Linux forwarding and NAT so traffic can reach the LXC bridge (vmbr0) and flow back.</p>
<p>On Proxmox host:</p>
<ol>
<li>Enable forwarding (persistent)</li>
</ol>
<ul>
<li>sysctl: net.ipv4.ip_forward=1</li>
</ul>
<ol start="2">
<li>NAT and forward rules (iptables examples)</li>
</ol>
<ul>
<li><p>NAT: iptables -t nat -A POSTROUTING -s 100.64.0.0/10 -o vmbr0 -j MASQUERADE</p>
</li>
<li><p>Forward allow: iptables -I FORWARD -s 100.64.0.0/10 -d 192.168.8.0/24 -j ACCEPT</p>
</li>
<li><p>Forward return: iptables -I FORWARD -s 192.168.8.0/24 -d 100.64.0.0/10 -m state --state RELATED,ESTABLISHED -j ACCEPT</p>
</li>
</ul>
<p>Note: 100.64.0.0/10 is the Tailscale CGNAT range. Adjust if you use a different mesh range.</p>
<hr />
<h2 id="heading-verify-ec2-to-container-connectivity">Verify EC2-to-container connectivity</h2>
<p>Run these quick checks after enabling routes and NAT:</p>
<p>From EC2 (after Tailscale + NAT):</p>
<pre><code class="lang-plaintext">ping 192.168.8.101
</code></pre>
<p>From Proxmox host:</p>
<pre><code class="lang-plaintext">tcpdump -i vmbr0 host 192.168.8.101
</code></pre>
<hr />
<h2 id="heading-troubleshooting-network-connectivity-amp-db-integration">Troubleshooting: Network connectivity &amp; DB integration</h2>
<p>All connectivity issues from “Network Connectivity &amp; Database Integration Troubleshooting” have been consolidated here.</p>
<h3 id="heading-symptoms-and-quick-diagnoses">Symptoms and quick diagnoses</h3>
<ul>
<li><p>EC2 → Proxmox host: ping works, but EC2 → ct-db: ping fails</p>
<ul>
<li>Likely cause: EC2 not accepting Tailscale subnet routes; or Proxmox not forwarding/NATing</li>
</ul>
</li>
<li><p>App timeout on DB connection</p>
<ul>
<li>Likely causes: wrong host IP, DB bound to <a target="_blank" href="http://localhost">localhost</a>, missing user/privileges, route not via tailscale0</li>
</ul>
</li>
</ul>
<h3 id="heading-fix-1-accept-subnet-routes-on-ec2">Fix 1: Accept subnet routes on EC2</h3>
<ul>
<li><p>sudo tailscale up --accept-routes</p>
</li>
<li><p>Confirm: tailscale status shows accepted routes; ip route get 192.168.8.101 returns dev tailscale0 (table 52)</p>
</li>
</ul>
<h3 id="heading-fix-2-enable-ip-forwarding-and-nat-on-proxmox">Fix 2: Enable IP forwarding and NAT on Proxmox</h3>
<ul>
<li><p>sysctl: net.ipv4.ip_forward=1 (persist via /etc/sysctl.conf)</p>
</li>
<li><p>iptables NAT: -t nat -A POSTROUTING -s 100.64.0.0/10 -d 192.168.8.0/24 -j MASQUERADE</p>
</li>
<li><p>iptables FORWARD: allow 100.64.0.0/10 ↔ 192.168.8.0/24 as shown above</p>
</li>
</ul>
<h3 id="heading-verification-commands">Verification commands</h3>
<p>EC2:</p>
<pre><code class="lang-plaintext">ping 192.168.8.101
ip route get 192.168.8.101
</code></pre>
<p>Proxmox host:</p>
<pre><code class="lang-plaintext">tailscale status
iptables -L -v -n
iptables -t nat -L -v -n
</code></pre>
<hr />
<h2 id="heading-final-checklist">Final checklist</h2>
<p>✅ Proxmox host advertises 192.168.8.0/24 via Tailscale</p>
<p>✅ EC2 accepts routes (ip route shows tailscale0 for 192.168.8.101)</p>
<p>✅ IP forwarding enabled; NAT + FORWARD rules applied</p>
<p>✅ ct-db reachable from EC2 (ping, optional DB port test)</p>
<p>✅ (Optional) MariaDB/PostgreSQL installed and listening on container interface</p>
<p>Next: deploy the frontend/backend, database schema, and monitoring in <s>Article 2</s>.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying a Full-Stack Application Across Hybrid Cloud Infrastructure]]></title><description><![CDATA[This guide covers deploying a simple full‑stack app with an AWS frontend and backend that talks to an on‑prem database running in a Proxmox LXC container, plus a basic monitoring stack. It assumes you’ve completed the network setup in Article 1 and c...]]></description><link>https://blog.sachindu.me/deploy-fullstack-app-hybrid-cloud-infrastructure</link><guid isPermaLink="true">https://blog.sachindu.me/deploy-fullstack-app-hybrid-cloud-infrastructure</guid><category><![CDATA[AWS]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[Hybrid Cloud]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[Grafana Monitoring]]></category><category><![CDATA[#prometheus]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Sun, 09 Nov 2025 17:06:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762713817861/5795a6c0-bab0-4899-8162-f67e8c9a4023.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This guide covers deploying a simple full‑stack app with an AWS frontend and backend that talks to an on‑prem database running in a Proxmox LXC container, plus a basic monitoring stack. It assumes you’ve completed the network setup in Article 1 and can reach ct-db from EC2 over Tailscale.</p>
<h2 id="heading-table-of-contents">Table of contents</h2>
<ul>
<li><p>Overview and architecture</p>
</li>
<li><p>Create AWS VPC (public + private subnets)</p>
</li>
<li><p>Provision EC2 instances (frontend public, backend private)</p>
</li>
<li><p>Configure frontend → backend communication (Nginx reverse proxy)</p>
</li>
<li><p>Create two LXC in Proxmox (ct-db, ct-monitor)</p>
</li>
<li><p>Proxmox host ↔ CT connectivity checks</p>
</li>
<li><p>Backend EC2 ↔ ct-db over Tailscale</p>
</li>
<li><p>Backend (Node.js + MariaDB client) setup</p>
</li>
<li><p>Database on ct-db (MariaDB recommended)</p>
</li>
<li><p>Monitoring on ct-monitor (Prometheus + Grafana)</p>
</li>
<li><p>End-to-end test and verification</p>
</li>
<li><p>Security hardening</p>
</li>
</ul>
<hr />
<h2 id="heading-overview-and-architecture">Overview and architecture</h2>
<p>High-level flow: A public frontend EC2 serves the website and proxies API requests to a private backend EC2. The backend talks to an on‑prem database (ct-db) over a Tailscale mesh to your Proxmox host. Monitoring runs in another LXC (ct-monitor).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762714670101/2838acb6-e061-4089-aaa0-4140f468dec0.jpeg" alt class="image--center mx-auto" /></p>
<p>Prereqs from Article 1:</p>
<ul>
<li><p>Proxmox with two LXC containers</p>
<ul>
<li><p>ct-db: 192.168.8.101 (MariaDB/PostgreSQL)</p>
</li>
<li><p>ct-monitor: 192.168.8.102 (Prometheus + Grafana)</p>
</li>
</ul>
</li>
<li><p>Tailscale configured</p>
<ul>
<li><p>Proxmox advertises 192.168.8.0/24</p>
</li>
<li><p>EC2 instances accept routes</p>
</li>
</ul>
</li>
<li><p>Verified EC2 → ct-db connectivity (ping + TCP/3306)</p>
</li>
</ul>
<hr />
<h2 id="heading-create-aws-vpc-public-private-subnets">Create AWS VPC (public + private subnets)</h2>
<p>Minimal setup:</p>
<ol>
<li><p>VPC with one public and one private subnet</p>
<ul>
<li><p>Public subnet: Internet Gateway + route for 0.0.0.0/0</p>
</li>
<li><p>Private subnet: NAT Gateway + route for 0.0.0.0/0 via NAT</p>
</li>
</ul>
</li>
<li><p>Route tables</p>
<ul>
<li><p>Associate the public subnet with the IGW route table</p>
</li>
<li><p>Associate the private subnet with the NAT route table</p>
</li>
</ul>
</li>
</ol>
<p>Keep the backend private; you’ll reach it from the frontend using its private IP and an Nginx reverse proxy.</p>
<hr />
<h2 id="heading-provision-ec2-instances-frontend-public-backend-private">Provision EC2 instances (frontend public, backend private)</h2>
<p>Create two EC2 instances in your VPC: one in the public subnet for the frontend and one in the private subnet for the backend.</p>
<h3 id="heading-frontend-ec2-public-subnet">Frontend EC2 (Public Subnet)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Configuration</td><td>Value</td></tr>
</thead>
<tbody>
<tr>
<td>AMI</td><td>Amazon Linux 2023 or Ubuntu 22.04 LTS</td></tr>
<tr>
<td>Instance Type</td><td>t2.micro or t2.nano</td></tr>
<tr>
<td>Subnet</td><td>Public subnet</td></tr>
<tr>
<td>Auto-assign Public IP</td><td>Enable</td></tr>
<tr>
<td>Security Group</td><td>SG-frontend</td></tr>
</tbody>
</table>
</div><p><strong>SG-frontend Rules:</strong></p>
<ul>
<li><p>Port 22 (SSH) from your IP</p>
</li>
<li><p>Port 80 (HTTP) from 0.0.0.0/0</p>
</li>
<li><p>Port 443 (HTTPS) from 0.0.0.0/0</p>
</li>
</ul>
<h3 id="heading-backend-ec2-private-subnet">Backend EC2 (Private Subnet)</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Configuration</td><td>Value</td></tr>
</thead>
<tbody>
<tr>
<td>AMI</td><td>Amazon Linux 2023 or Ubuntu 22.04 LTS</td></tr>
<tr>
<td>Instance Type</td><td>t2.micro or t2.nano</td></tr>
<tr>
<td>Subnet</td><td>Private subnet</td></tr>
<tr>
<td>Auto-assign Public IP</td><td>Disable</td></tr>
<tr>
<td>Security Group</td><td>SG-backend</td></tr>
</tbody>
</table>
</div><p><strong>SG-backend Rules:</strong></p>
<ul>
<li><p>Port 22 (SSH) from SG-frontend</p>
</li>
<li><p>Port 3000 (TCP) from SG-frontend</p>
</li>
</ul>
<hr />
<h2 id="heading-ssh-access-and-bastion-setup">SSH Access and Bastion Setup</h2>
<h3 id="heading-connect-to-frontend-ec2">Connect to Frontend EC2</h3>
<p>Log into the frontend VM using SSH with your PEM key:</p>
<pre><code class="lang-bash">ssh -i hybrid-cloud.pem ec2-user@&lt;FRONTEND_PUBLIC_IP&gt;
</code></pre>
<h3 id="heading-connect-to-backend-ec2-via-bastion">Connect to Backend EC2 (via bastion)</h3>
<p>The backend instance has no public IP, so use the frontend as a bastion host.</p>
<p><strong>Step 1:</strong> Copy the PEM key to the frontend instance:</p>
<pre><code class="lang-bash">scp -i hybrid-cloud.pem hybrid-cloud.pem ec2-user@&lt;FRONTEND_PUBLIC_IP&gt;:/home/ec2-user/
</code></pre>
<p><strong>Step 2:</strong> SSH from the frontend to the backend using its private IP:</p>
<pre><code class="lang-bash">ssh -i ~/hybrid-cloud.pem ec2-user@&lt;BACKEND_PRIVATE_IP&gt;
</code></pre>
<hr />
<h2 id="heading-configure-frontend-nginx-static-site-reverse-proxy">Configure Frontend (Nginx + Static Site + Reverse Proxy)</h2>
<p>This section covers installing Nginx, creating the static HTML pages, and configuring the reverse proxy to route API requests to the backend.</p>
<h3 id="heading-install-and-enable-nginx">Install and Enable Nginx</h3>
<p>Update packages and install Nginx:</p>
<pre><code class="lang-bash">sudo yum update -y &amp;&amp; sudo yum upgrade -y
sudo yum install -y nginx
sudo systemctl <span class="hljs-built_in">enable</span> --now nginx
</code></pre>
<h3 id="heading-create-web-root-and-html-files">Create Web Root and HTML Files</h3>
<p>Create the web root directory:</p>
<pre><code class="lang-bash">sudo mkdir -p /var/www/html
<span class="hljs-built_in">cd</span> /var/www/html
</code></pre>
<p>Create <code>index.html</code>:</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Hybrid Cloud Platform<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    * {
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">box-sizing</span>: border-box;
    }

    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Segoe UI'</span>, Tahoma, Geneva, Verdana, sans-serif;
      <span class="hljs-attribute">background</span>: <span class="hljs-built_in">linear-gradient</span>(<span class="hljs-number">135deg</span>, #<span class="hljs-number">31694</span>E <span class="hljs-number">0%</span>, #<span class="hljs-number">658</span>C58 <span class="hljs-number">100%</span>);
      <span class="hljs-attribute">min-height</span>: <span class="hljs-number">100vh</span>;
      <span class="hljs-attribute">display</span>: flex;
      <span class="hljs-attribute">justify-content</span>: center;
      <span class="hljs-attribute">align-items</span>: center;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
    }

    <span class="hljs-selector-class">.container</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#F0E491</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">20px</span> <span class="hljs-number">60px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.3</span>);
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">60px</span> <span class="hljs-number">40px</span>;
      <span class="hljs-attribute">max-width</span>: <span class="hljs-number">600px</span>;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
      <span class="hljs-attribute">text-align</span>: center;
    }

    <span class="hljs-selector-tag">h1</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">2.5em</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">15px</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">700</span>;
    }

    <span class="hljs-selector-class">.subtitle</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#658C58</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.2em</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">40px</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">500</span>;
    }

    <span class="hljs-selector-class">.project-scope</span> {
      <span class="hljs-attribute">background</span>: white;
      <span class="hljs-attribute">border-left</span>: <span class="hljs-number">5px</span> solid <span class="hljs-number">#BBC863</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">30px</span> <span class="hljs-number">0</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>;
      <span class="hljs-attribute">text-align</span>: left;
    }

    <span class="hljs-selector-class">.project-scope</span> <span class="hljs-selector-tag">h2</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.3em</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">10px</span>;
    }

    <span class="hljs-selector-class">.project-scope</span> <span class="hljs-selector-tag">p</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>;
      <span class="hljs-attribute">line-height</span>: <span class="hljs-number">1.6</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.05em</span>;
    }

    <span class="hljs-selector-class">.tech-badges</span> {
      <span class="hljs-attribute">display</span>: flex;
      <span class="hljs-attribute">justify-content</span>: center;
      <span class="hljs-attribute">gap</span>: <span class="hljs-number">15px</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">20px</span> <span class="hljs-number">0</span>;
      <span class="hljs-attribute">flex-wrap</span>: wrap;
    }

    <span class="hljs-selector-class">.badge</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#658C58</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#F0E491</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">8px</span> <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.9em</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
    }

    <span class="hljs-selector-class">.cta-button</span> {
      <span class="hljs-attribute">display</span>: inline-block;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#F0E491</span>;
      <span class="hljs-attribute">text-decoration</span>: none;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">18px</span> <span class="hljs-number">50px</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">50px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.3em</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">700</span>;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">30px</span>;
      <span class="hljs-attribute">transition</span>: all <span class="hljs-number">0.3s</span> ease;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">8px</span> <span class="hljs-number">20px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">49</span>, <span class="hljs-number">105</span>, <span class="hljs-number">78</span>, <span class="hljs-number">0.4</span>);
      <span class="hljs-attribute">position</span>: relative;
      <span class="hljs-attribute">overflow</span>: hidden;
    }

    <span class="hljs-selector-class">.cta-button</span><span class="hljs-selector-pseudo">::before</span> {
      <span class="hljs-attribute">content</span>: <span class="hljs-string">''</span>;
      <span class="hljs-attribute">position</span>: absolute;
      <span class="hljs-attribute">top</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">left</span>: -<span class="hljs-number">100%</span>;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
      <span class="hljs-attribute">height</span>: <span class="hljs-number">100%</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-built_in">linear-gradient</span>(<span class="hljs-number">90deg</span>, transparent, rgba(<span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">255</span>, <span class="hljs-number">0.2</span>), transparent);
      <span class="hljs-attribute">transition</span>: left <span class="hljs-number">0.5s</span>;
    }

    <span class="hljs-selector-class">.cta-button</span><span class="hljs-selector-pseudo">:hover</span><span class="hljs-selector-pseudo">::before</span> {
      <span class="hljs-attribute">left</span>: <span class="hljs-number">100%</span>;
    }

    <span class="hljs-selector-class">.cta-button</span><span class="hljs-selector-pseudo">:hover</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#254d3a</span>;
      <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateY</span>(-<span class="hljs-number">3px</span>);
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">12px</span> <span class="hljs-number">30px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">49</span>, <span class="hljs-number">105</span>, <span class="hljs-number">78</span>, <span class="hljs-number">0.5</span>);
    }

    <span class="hljs-selector-class">.highlight-box</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#BBC863</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">5px</span> <span class="hljs-number">15px</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">5px</span>;
      <span class="hljs-attribute">display</span>: inline-block;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">700</span>;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.95em</span>;
    }

    <span class="hljs-keyword">@media</span> (<span class="hljs-attribute">max-width:</span> <span class="hljs-number">600px</span>) {
      <span class="hljs-selector-class">.container</span> {
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">40px</span> <span class="hljs-number">25px</span>;
      }

      <span class="hljs-selector-tag">h1</span> {
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">2em</span>;
      }

      <span class="hljs-selector-class">.subtitle</span> {
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1em</span>;
      }
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>🚀 Hybrid Cloud Platform<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"subtitle"</span>&gt;</span>Seamless Integration Across Environments<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"project-scope"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Project Scope<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>Hybrid Cloud Infrastructure:<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span> AWS Cloud + Proxmox Homelab<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"tech-badges"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"badge"</span>&gt;</span>AWS<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"badge"</span>&gt;</span>Proxmox<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"badge"</span>&gt;</span>Homelab<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"register.html"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"cta-button"</span>&gt;</span>Register as a User<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Create <code>register.html</code>:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Register - Hybrid Cloud Platform<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    * {
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">box-sizing</span>: border-box;
    }

    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Segoe UI'</span>, Tahoma, Geneva, Verdana, sans-serif;
      <span class="hljs-attribute">background</span>: <span class="hljs-built_in">linear-gradient</span>(<span class="hljs-number">135deg</span>, #<span class="hljs-number">31694</span>E <span class="hljs-number">0%</span>, #<span class="hljs-number">658</span>C58 <span class="hljs-number">100%</span>);
      <span class="hljs-attribute">min-height</span>: <span class="hljs-number">100vh</span>;
      <span class="hljs-attribute">display</span>: flex;
      <span class="hljs-attribute">justify-content</span>: center;
      <span class="hljs-attribute">align-items</span>: center;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
    }

    <span class="hljs-selector-class">.container</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#F0E491</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">20px</span> <span class="hljs-number">60px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.3</span>);
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">50px</span> <span class="hljs-number">40px</span>;
      <span class="hljs-attribute">max-width</span>: <span class="hljs-number">500px</span>;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
    }

    <span class="hljs-selector-class">.header</span> {
      <span class="hljs-attribute">text-align</span>: center;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">40px</span>;
    }

    <span class="hljs-selector-tag">h2</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">2.2em</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">700</span>;
    }

    <span class="hljs-selector-class">.subtitle</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#658C58</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1em</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">10px</span>;
    }

    <span class="hljs-selector-class">.back-link</span> {
      <span class="hljs-attribute">display</span>: inline-block;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">text-decoration</span>: none;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.95em</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">transition</span>: color <span class="hljs-number">0.3s</span>;
    }

    <span class="hljs-selector-class">.back-link</span><span class="hljs-selector-pseudo">:hover</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#658C58</span>;
    }

    <span class="hljs-selector-class">.form-group</span> {
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">25px</span>;
    }

    <span class="hljs-selector-tag">label</span> {
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
      <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">8px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.95em</span>;
    }

    <span class="hljs-selector-tag">input</span><span class="hljs-selector-attr">[type=<span class="hljs-string">"text"</span>]</span>,
    <span class="hljs-selector-tag">input</span><span class="hljs-selector-attr">[type=<span class="hljs-string">"password"</span>]</span> {
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">15px</span>;
      <span class="hljs-attribute">border</span>: <span class="hljs-number">2px</span> solid <span class="hljs-number">#BBC863</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1em</span>;
      <span class="hljs-attribute">background</span>: white;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">transition</span>: all <span class="hljs-number">0.3s</span>;
      <span class="hljs-attribute">outline</span>: none;
    }

    <span class="hljs-selector-tag">input</span><span class="hljs-selector-attr">[type=<span class="hljs-string">"text"</span>]</span><span class="hljs-selector-pseudo">:focus</span>,
    <span class="hljs-selector-tag">input</span><span class="hljs-selector-attr">[type=<span class="hljs-string">"password"</span>]</span><span class="hljs-selector-pseudo">:focus</span> {
      <span class="hljs-attribute">border-color</span>: <span class="hljs-number">#658C58</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">3px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">101</span>, <span class="hljs-number">140</span>, <span class="hljs-number">88</span>, <span class="hljs-number">0.1</span>);
    }

    <span class="hljs-selector-tag">input</span><span class="hljs-selector-pseudo">::placeholder</span> {
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#BBC863</span>;
    }

    <span class="hljs-selector-class">.btn-submit</span> {
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">16px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#F0E491</span>;
      <span class="hljs-attribute">border</span>: none;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">10px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.1em</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">700</span>;
      <span class="hljs-attribute">cursor</span>: pointer;
      <span class="hljs-attribute">transition</span>: all <span class="hljs-number">0.3s</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">6px</span> <span class="hljs-number">20px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">49</span>, <span class="hljs-number">105</span>, <span class="hljs-number">78</span>, <span class="hljs-number">0.3</span>);
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">10px</span>;
    }

    <span class="hljs-selector-class">.btn-submit</span><span class="hljs-selector-pseudo">:hover</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#254d3a</span>;
      <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateY</span>(-<span class="hljs-number">2px</span>);
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">8px</span> <span class="hljs-number">25px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">49</span>, <span class="hljs-number">105</span>, <span class="hljs-number">78</span>, <span class="hljs-number">0.4</span>);
    }

    <span class="hljs-selector-class">.btn-submit</span><span class="hljs-selector-pseudo">:active</span> {
      <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateY</span>(<span class="hljs-number">0</span>);
    }

    <span class="hljs-selector-class">.btn-submit</span><span class="hljs-selector-pseudo">:disabled</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#BBC863</span>;
      <span class="hljs-attribute">cursor</span>: not-allowed;
      <span class="hljs-attribute">transform</span>: none;
    }

    <span class="hljs-selector-class">.info-box</span> {
      <span class="hljs-attribute">background</span>: white;
      <span class="hljs-attribute">border-left</span>: <span class="hljs-number">4px</span> solid <span class="hljs-number">#BBC863</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">15px</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">25px</span>;
      <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.9em</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
    }

    <span class="hljs-selector-class">.alert</span> {
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">15px</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">500</span>;
      <span class="hljs-attribute">display</span>: none;
    }

    <span class="hljs-selector-class">.alert</span><span class="hljs-selector-class">.success</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#BBC863</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#31694E</span>;
      <span class="hljs-attribute">display</span>: block;
    }

    <span class="hljs-selector-class">.alert</span><span class="hljs-selector-class">.error</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#ff6b6b</span>;
      <span class="hljs-attribute">color</span>: white;
      <span class="hljs-attribute">display</span>: block;
    }

    <span class="hljs-keyword">@media</span> (<span class="hljs-attribute">max-width:</span> <span class="hljs-number">600px</span>) {
      <span class="hljs-selector-class">.container</span> {
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">35px</span> <span class="hljs-number">25px</span>;
      }

      <span class="hljs-selector-tag">h2</span> {
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.8em</span>;
      }
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"index.html"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"back-link"</span>&gt;</span>← Back to Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"header"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Create Account<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"subtitle"</span>&gt;</span>Join the Hybrid Cloud Platform<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"regForm"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"username"</span>&gt;</span>Username<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"username"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Enter your username"</span> <span class="hljs-attr">required</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"password"</span>&gt;</span>Password<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Enter your password"</span> <span class="hljs-attr">required</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn-submit"</span>&gt;</span>Register Account<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"alertBox"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"alert"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info-box"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>🔒 Secure Registration<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">br</span>&gt;</span>
      Your credentials will be securely stored in our hybrid cloud infrastructure.
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'regForm'</span>).addEventListener(<span class="hljs-string">'submit'</span>, <span class="hljs-keyword">async</span> (e) =&gt; {
      e.preventDefault();

      <span class="hljs-keyword">const</span> alertBox = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'alertBox'</span>);
      <span class="hljs-keyword">const</span> submitBtn = e.target.querySelector(<span class="hljs-string">'.btn-submit'</span>);
      <span class="hljs-keyword">const</span> username = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'username'</span>).value;
      <span class="hljs-keyword">const</span> password = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'password'</span>).value;

      <span class="hljs-comment">// Reset alert</span>
      alertBox.className = <span class="hljs-string">'alert'</span>;
      alertBox.textContent = <span class="hljs-string">''</span>;

      <span class="hljs-comment">// Disable button during request</span>
      submitBtn.disabled = <span class="hljs-literal">true</span>;
      submitBtn.textContent = <span class="hljs-string">'Registering...'</span>;

      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/register'</span>, {
          <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
          <span class="hljs-attr">headers</span>: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
          <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ username, password })
        });

        <span class="hljs-comment">// Check if response is ok</span>
        <span class="hljs-keyword">if</span> (!res.ok) {
          <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Server returned <span class="hljs-subst">${res.status}</span>: <span class="hljs-subst">${res.statusText}</span>`</span>);
        }

        <span class="hljs-comment">// Check if response is JSON</span>
        <span class="hljs-keyword">const</span> contentType = res.headers.get(<span class="hljs-string">'content-type'</span>);
        <span class="hljs-keyword">if</span> (!contentType || !contentType.includes(<span class="hljs-string">'application/json'</span>)) {
          <span class="hljs-keyword">const</span> text = <span class="hljs-keyword">await</span> res.text();
          <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Expected JSON but got: <span class="hljs-subst">${text.substring(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)}</span>`</span>);
        }

        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();

        <span class="hljs-keyword">if</span> (data.success) {
          alertBox.className = <span class="hljs-string">'alert success'</span>;
          alertBox.textContent = <span class="hljs-string">`✓ User <span class="hljs-subst">${data.user.username}</span> registered successfully!`</span>;
          <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'regForm'</span>).reset();
        } <span class="hljs-keyword">else</span> {
          alertBox.className = <span class="hljs-string">'alert error'</span>;
          alertBox.textContent = <span class="hljs-string">`✗ Error: <span class="hljs-subst">${data.message}</span>`</span>;
        }
      } <span class="hljs-keyword">catch</span> (error) {
        alertBox.className = <span class="hljs-string">'alert error'</span>;
        alertBox.textContent = <span class="hljs-string">`✗ Error: <span class="hljs-subst">${error.message}</span>`</span>;
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Registration error:'</span>, error);
      } <span class="hljs-keyword">finally</span> {
        <span class="hljs-comment">// Re-enable button</span>
        submitBtn.disabled = <span class="hljs-literal">false</span>;
        submitBtn.textContent = <span class="hljs-string">'Register Account'</span>;
      }
    });
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<h3 id="heading-configure-nginx-reverse-proxy">Configure Nginx Reverse Proxy</h3>
<p>Edit the Nginx configuration (Amazon Linux path: <code>/etc/nginx/nginx.conf</code> or create a file under <code>/etc/nginx/conf.d/</code>):</p>
<pre><code class="lang-nginx"><span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
    <span class="hljs-attribute">server_name</span> _;

    <span class="hljs-attribute">root</span> /var/www/html;
    <span class="hljs-attribute">index</span> index.html;

    <span class="hljs-attribute">location</span> /api/ {
        <span class="hljs-attribute">proxy_pass</span> http://&lt;BACKEND_PRIVATE_IP&gt;:3000/;
        <span class="hljs-attribute">proxy_http_version</span> <span class="hljs-number">1</span>.<span class="hljs-number">1</span>;
        <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
    }
}
</code></pre>
<p>Replace <code>&lt;BACKEND_PRIVATE_IP&gt;</code> with your backend EC2's private IP address.</p>
<h3 id="heading-restart-nginx">Restart Nginx</h3>
<p>Restart Nginx to apply the configuration:</p>
<pre><code class="lang-bash">sudo systemctl restart nginx
</code></pre>
<h3 id="heading-test-connectivity">Test Connectivity</h3>
<p>Optional: verify the frontend can reach the backend:</p>
<pre><code class="lang-bash">ping &lt;BACKEND_PRIVATE_IP&gt; -c 5
</code></pre>
<hr />
<h2 id="heading-configure-backend-nodejs-api">Configure Backend (Node.js API)</h2>
<p>This section covers setting up the Node.js backend API that connects to the ct-db database over Tailscale.</p>
<h3 id="heading-install-nodejs-and-dependencies">Install Node.js and Dependencies</h3>
<p>On the backend EC2 instance, install Node.js and create the application directory</p>
<pre><code class="lang-bash">sudo yum update -y &amp;&amp; sudo yum upgrade -y
sudo yum install -y nodejs npm
mkdir ~/simple-app &amp;&amp; <span class="hljs-built_in">cd</span> ~/simple-app
npm init -y
npm install express body-parser cors mariadb
sudo yum install mariadb105 -y
</code></pre>
<h3 id="heading-create-the-backend-api-serverjs">Create the Backend API (server.js)</h3>
<p>Create <code>server.js</code> in the <code>~/simple-app</code> directory:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> bodyParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'body-parser'</span>);
<span class="hljs-keyword">const</span> mariadb = <span class="hljs-built_in">require</span>(<span class="hljs-string">'mariadb'</span>);
<span class="hljs-keyword">const</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cors'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

<span class="hljs-keyword">const</span> app = express();

<span class="hljs-comment">// Middleware</span>
app.use(cors());
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, <span class="hljs-string">'public'</span>))); <span class="hljs-comment">// Serve static files</span>

<span class="hljs-comment">// Create MariaDB pool with increased timeouts for VPN connection</span>
<span class="hljs-keyword">const</span> pool = mariadb.createPool({
  <span class="hljs-attr">host</span>: <span class="hljs-string">'192.168.8.101'</span>,
  <span class="hljs-attr">user</span>: <span class="hljs-string">'appuser'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'StrongPassword123'</span>,
  <span class="hljs-attr">database</span>: <span class="hljs-string">'appdb'</span>,
  <span class="hljs-attr">port</span>: <span class="hljs-number">3306</span>,
  <span class="hljs-attr">connectionLimit</span>: <span class="hljs-number">10</span>,
  <span class="hljs-attr">connectTimeout</span>: <span class="hljs-number">30000</span>,        <span class="hljs-comment">// 30 seconds (default is 1000ms)</span>
  <span class="hljs-attr">acquireTimeout</span>: <span class="hljs-number">30000</span>,         <span class="hljs-comment">// 30 seconds for pool acquisition</span>
  <span class="hljs-attr">timeout</span>: <span class="hljs-number">30000</span>,                <span class="hljs-comment">// 30 seconds for queries</span>
  <span class="hljs-attr">socketTimeout</span>: <span class="hljs-number">30000</span>,          <span class="hljs-comment">// 30 seconds for socket operations</span>
  <span class="hljs-attr">initializationTimeout</span>: <span class="hljs-number">30000</span>   <span class="hljs-comment">// 30 seconds for pool initialization</span>
});

<span class="hljs-comment">// Test database connection on startup</span>
(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> conn = <span class="hljs-keyword">await</span> pool.getConnection();
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'✓ Database connected successfully'</span>);
    conn.release();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'✗ Database connection failed:'</span>, err);
  }
})();

<span class="hljs-comment">// API Routes</span>
app.post(<span class="hljs-string">'/api/register'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> { username, password } = req.body;

  <span class="hljs-comment">// Validation</span>
  <span class="hljs-keyword">if</span> (!username || !password) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ 
      <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Username and password required'</span> 
    });
  }

  <span class="hljs-keyword">if</span> (username.length &lt; <span class="hljs-number">3</span>) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ 
      <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Username must be at least 3 characters'</span> 
    });
  }

  <span class="hljs-keyword">if</span> (password.length &lt; <span class="hljs-number">6</span>) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ 
      <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Password must be at least 6 characters'</span> 
    });
  }

  <span class="hljs-keyword">let</span> conn;
  <span class="hljs-keyword">try</span> {
    conn = <span class="hljs-keyword">await</span> pool.getConnection();

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> conn.query(
      <span class="hljs-string">'INSERT INTO users (username, password) VALUES (?, ?)'</span>,
      [username, password]
    );

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`✓ User registered: <span class="hljs-subst">${username}</span> (ID: <span class="hljs-subst">${result.insertId}</span>)`</span>);

    res.json({
      <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">message</span>: <span class="hljs-string">'User registered successfully!'</span>,
      <span class="hljs-attr">user</span>: {
        <span class="hljs-attr">username</span>: username,
        <span class="hljs-attr">id</span>: result.insertId.toString()
      }
    });

  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Database error:'</span>, err);

    <span class="hljs-comment">// Handle duplicate username</span>
    <span class="hljs-keyword">if</span> (err.code === <span class="hljs-string">'ER_DUP_ENTRY'</span>) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">409</span>).json({
        <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">message</span>: <span class="hljs-string">'Username already exists'</span>
      });
    }

    <span class="hljs-comment">// Generic server error</span>
    res.status(<span class="hljs-number">500</span>).json({ 
      <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error occurred'</span> 
    });

  } <span class="hljs-keyword">finally</span> {
    <span class="hljs-keyword">if</span> (conn) conn.release();
  }
});

<span class="hljs-comment">// Get all users (for testing/admin purposes)</span>
app.get(<span class="hljs-string">'/api/users'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">let</span> conn;
  <span class="hljs-keyword">try</span> {
    conn = <span class="hljs-keyword">await</span> pool.getConnection();
    <span class="hljs-keyword">const</span> rows = <span class="hljs-keyword">await</span> conn.query(<span class="hljs-string">'SELECT id, username, created_at FROM users ORDER BY created_at DESC'</span>);

    res.json({
      <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">users</span>: rows
    });

  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Database error:'</span>, err);
    res.status(<span class="hljs-number">500</span>).json({ 
      <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> 
    });
  } <span class="hljs-keyword">finally</span> {
    <span class="hljs-keyword">if</span> (conn) conn.release();
  }
});

<span class="hljs-comment">// Health check endpoint</span>
app.get(<span class="hljs-string">'/api/health'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">let</span> dbStatus = <span class="hljs-string">'disconnected'</span>;

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> conn = <span class="hljs-keyword">await</span> pool.getConnection();
    dbStatus = <span class="hljs-string">'connected'</span>;
    conn.release();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Health check failed:'</span>, err);
  }

  res.json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">'running'</span>,
    <span class="hljs-attr">database</span>: dbStatus,
    <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
  });
});

<span class="hljs-comment">// 404 handler</span>
app.use(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.status(<span class="hljs-number">404</span>).json({ 
    <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
    <span class="hljs-attr">message</span>: <span class="hljs-string">'Endpoint not found'</span> 
  });
});

<span class="hljs-comment">// Error handler</span>
app.use(<span class="hljs-function">(<span class="hljs-params">err, req, res, next</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Server error:'</span>, err);
  res.status(<span class="hljs-number">500</span>).json({ 
    <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, 
    <span class="hljs-attr">message</span>: <span class="hljs-string">'Internal server error'</span> 
  });
});

<span class="hljs-comment">// Start server</span>
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3000</span>;
app.listen(PORT, <span class="hljs-string">'0.0.0.0'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'================================='</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`🚀 Server running on port <span class="hljs-subst">${PORT}</span>`</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`📡 Access at: http://localhost:<span class="hljs-subst">${PORT}</span>`</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'================================='</span>);
});
</code></pre>
<h3 id="heading-run-the-backend-server">Run the Backend Server</h3>
<p>Start the Node.js server:</p>
<pre><code class="lang-bash">node server.js
</code></pre>
<p>You should see output like:</p>
<pre><code class="lang-plaintext">=================================
🚀 Server running on port 3000
📡 Access at: http://localhost:3000
=================================
</code></pre>
<hr />
<h2 id="heading-create-two-lxc-containers-in-proxmox">Create Two LXC Containers in Proxmox</h2>
<p>This section covers creating two LXC containers on your Proxmox host: one for the database (ct-db) and one for monitoring (ct-monitor).</p>
<h3 id="heading-download-lxc-template">Download LXC Template</h3>
<p>In the Proxmox web UI:</p>
<ol>
<li><p>Navigate to <strong>Datacenter → proxmox → local (storage) → Templates</strong></p>
</li>
<li><p>Click <strong>Templates</strong> button</p>
</li>
<li><p>Download a Debian or Ubuntu template (e.g., <code>ubuntu-22.04-standard</code>)</p>
</li>
</ol>
<h3 id="heading-create-ct-db-container-database">Create ct-db Container (Database)</h3>
<p>Click <strong>Create CT</strong> and configure:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Value</td></tr>
</thead>
<tbody>
<tr>
<td>Node</td><td>proxmox</td></tr>
<tr>
<td>CT ID</td><td>101</td></tr>
<tr>
<td>Hostname</td><td>ct-db</td></tr>
<tr>
<td>Password</td><td>(choose a secure password)</td></tr>
<tr>
<td>Template</td><td>Debian/Ubuntu template downloaded above</td></tr>
<tr>
<td>Disk Size</td><td>10 GB</td></tr>
<tr>
<td>CPU</td><td>2 cores</td></tr>
<tr>
<td>Memory</td><td>2048 MB (2 GB)</td></tr>
<tr>
<td>Swap</td><td>512 MB</td></tr>
<tr>
<td>Network - Bridge</td><td>vmbr0</td></tr>
<tr>
<td>IPv4</td><td>Static</td></tr>
<tr>
<td>IPv4/CIDR</td><td>192.168.8.101/24</td></tr>
<tr>
<td>Gateway</td><td>192.168.8.1 <em>(Router Gateway Address)</em></td></tr>
<tr>
<td>DNS Server</td><td>8.8.8.8</td></tr>
</tbody>
</table>
</div><p>After creation, enable "Start at boot" in ct-db → Options.</p>
<h3 id="heading-create-ct-monitor-container-monitoring">Create ct-monitor Container (Monitoring)</h3>
<p>Click <strong>Create CT</strong> and configure:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Value</td></tr>
</thead>
<tbody>
<tr>
<td>Node</td><td>proxmox</td></tr>
<tr>
<td>CT ID</td><td>102</td></tr>
<tr>
<td>Hostname</td><td>ct-monitor</td></tr>
<tr>
<td>Password</td><td>(choose a secure password)</td></tr>
<tr>
<td>Template</td><td>Debian/Ubuntu template downloaded above</td></tr>
<tr>
<td>Disk Size</td><td>10 GB</td></tr>
<tr>
<td>CPU</td><td>2 cores</td></tr>
<tr>
<td>Memory</td><td>2048 MB (2 GB)</td></tr>
<tr>
<td>Swap</td><td>512 MB</td></tr>
<tr>
<td>Network - Bridge</td><td>vmbr0</td></tr>
<tr>
<td>IPv4</td><td>Static</td></tr>
<tr>
<td>IPv4/CIDR</td><td>192.168.8.102/24</td></tr>
<tr>
<td>Gateway</td><td>192.168.8.1 <em>(Router Gateway Address)</em></td></tr>
<tr>
<td>DNS Server</td><td>8.8.8.8</td></tr>
</tbody>
</table>
</div><p>After creation, enable "Start at boot" in ct-monitor → Options.</p>
<hr />
<h2 id="heading-configure-database-on-ct-db">Configure Database on ct-db</h2>
<p>This section covers installing MariaDB, creating the database and user, and setting up the users table.</p>
<h3 id="heading-install-and-start-mariadb">Install and Start MariaDB</h3>
<p>Access the ct-db container console from Proxmox UI or use:</p>
<pre><code class="lang-bash">pct enter 101
</code></pre>
<p>Update and install MariaDB:</p>
<pre><code class="lang-bash">sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install -y mariadb-server
sudo systemctl <span class="hljs-built_in">enable</span> mariadb
sudo systemctl start mariadb
</code></pre>
<h3 id="heading-create-database-and-user">Create Database and User</h3>
<p>Log into MariaDB:</p>
<pre><code class="lang-bash">mysql -u root -p
</code></pre>
<p>Run the following SQL commands:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create database</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> appdb;

<span class="hljs-comment">-- Show current users and their hosts</span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">User</span>, Host <span class="hljs-keyword">FROM</span> mysql.user;

<span class="hljs-comment">-- Create user with remote access</span>
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">ON</span> appdb.* <span class="hljs-keyword">TO</span> <span class="hljs-string">'appuser'</span>@<span class="hljs-string">'%'</span> <span class="hljs-keyword">IDENTIFIED</span> <span class="hljs-keyword">BY</span> <span class="hljs-string">'StrongPassword123'</span>;
<span class="hljs-keyword">FLUSH</span> <span class="hljs-keyword">PRIVILEGES</span>;

<span class="hljs-comment">-- Verify the grant</span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">User</span>, Host <span class="hljs-keyword">FROM</span> mysql.user <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">User</span>=<span class="hljs-string">'appuser'</span>;

<span class="hljs-comment">-- Switch to database</span>
<span class="hljs-keyword">USE</span> appdb;

<span class="hljs-comment">-- Create users table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> <span class="hljs-keyword">users</span> (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">INT</span> AUTO_INCREMENT PRIMARY <span class="hljs-keyword">KEY</span>,
  username <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">password</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>
);

<span class="hljs-comment">-- Verify table structure</span>
<span class="hljs-keyword">DESCRIBE</span> <span class="hljs-keyword">users</span>;

EXIT;
</code></pre>
<h3 id="heading-configure-remote-access">Configure Remote Access</h3>
<p>Edit the MariaDB configuration to allow remote connections:</p>
<pre><code class="lang-bash">sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
</code></pre>
<p>Find the <code>bind-address</code> line and change it to:</p>
<pre><code class="lang-plaintext">bind-address = 0.0.0.0
</code></pre>
<p>Restart MariaDB:</p>
<pre><code class="lang-bash">sudo systemctl restart mariadb
</code></pre>
<h2 id="heading-configure-monitoring-setup-on-ct-monitor-prometheus-grafana-node-exporter">Configure Monitoring Setup on ct-monitor (Prometheus + Grafana + Node Exporter)</h2>
<p>This comprehensive section covers installing and configuring Prometheus, Grafana, and Node Exporter across all machines.</p>
<h3 id="heading-1-install-on-ct-monitor-1921688102">1. Install on ct-monitor (192.168.8.102)</h3>
<p>Access the ct-monitor container:</p>
<pre><code class="lang-bash">pct enter 102
</code></pre>
<h4 id="heading-install-prometheus">Install Prometheus</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># Access container</span>
pct enter 102

<span class="hljs-comment"># Update system</span>
apt update &amp;&amp; apt upgrade -y

<span class="hljs-comment"># === PROMETHEUS ===</span>
useradd --no-create-home --shell /bin/<span class="hljs-literal">false</span> prometheus
<span class="hljs-built_in">cd</span> /tmp
wget https://github.com/prometheus/prometheus/releases/download/v2.47.0/prometheus-2.47.0.linux-amd64.tar.gz
tar -xvf prometheus-2.47.0.linux-amd64.tar.gz
<span class="hljs-built_in">cd</span> prometheus-2.47.0.linux-amd64
mv prometheus promtool /usr/<span class="hljs-built_in">local</span>/bin/
mkdir -p /etc/prometheus /var/lib/prometheus
mv consoles console_libraries /etc/prometheus/
chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus

<span class="hljs-comment"># Create Prometheus config (only 2 targets)</span>
cat &gt; /etc/prometheus/prometheus.yml &lt;&lt;EOF
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: <span class="hljs-string">'prometheus'</span>
    static_configs:
      - targets: [<span class="hljs-string">'localhost:9090'</span>]

  - job_name: <span class="hljs-string">'ct-monitor'</span>
    static_configs:
      - targets: [<span class="hljs-string">'localhost:9100'</span>]

  - job_name: <span class="hljs-string">'backend-ec2'</span>
    static_configs:
      - targets: [<span class="hljs-string">'BACKEND_TAILSCALE_IP:9100'</span>]
EOF

chown prometheus:prometheus /etc/prometheus/prometheus.yml

<span class="hljs-comment"># Create Prometheus service</span>
cat &gt; /etc/systemd/system/prometheus.service &lt;&lt;EOF
[Unit]
Description=Prometheus
After=network.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/<span class="hljs-built_in">local</span>/bin/prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --storage.tsdb.path=/var/lib/prometheus/ \
  --web.console.templates=/etc/prometheus/consoles \
  --web.console.libraries=/etc/prometheus/console_libraries

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl start prometheus
systemctl <span class="hljs-built_in">enable</span> prometheus
</code></pre>
<h4 id="heading-install-node-exporter">Install Node Exporter</h4>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
tar -xvf node_exporter-1.6.1.linux-amd64.tar.gz
mv node_exporter-1.6.1.linux-amd64/node_exporter /usr/<span class="hljs-built_in">local</span>/bin/
useradd --no-create-home --shell /bin/<span class="hljs-literal">false</span> node_exporter

cat &gt; /etc/systemd/system/node_exporter.service &lt;&lt;EOF
[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/<span class="hljs-built_in">local</span>/bin/node_exporter

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl start node_exporter
systemctl <span class="hljs-built_in">enable</span> node_exporter
</code></pre>
<p>Install <strong>Grafana</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># === GRAFANA ===</span>
apt install -y software-properties-common apt-transport-https wget
wget -q -O - https://packages.grafana.com/gpg.key | apt-key add -
<span class="hljs-built_in">echo</span> <span class="hljs-string">"deb https://packages.grafana.com/oss/deb stable main"</span> | tee /etc/apt/sources.list.d/grafana.list
apt update
apt install -y grafana

systemctl start grafana-server
systemctl <span class="hljs-built_in">enable</span> grafana-server

<span class="hljs-comment"># Verify all services</span>
systemctl status prometheus
systemctl status node_exporter
systemctl status grafana-server
</code></pre>
<h3 id="heading-2-install-on-backend-ec2">2. Install on Backend EC2</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># SSH to Backend EC2</span>
<span class="hljs-built_in">cd</span> /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
tar -xvf node_exporter-1.6.1.linux-amd64.tar.gz
sudo mv node_exporter-1.6.1.linux-amd64/node_exporter /usr/<span class="hljs-built_in">local</span>/bin/
sudo useradd --no-create-home --shell /bin/<span class="hljs-literal">false</span> node_exporter

sudo tee /etc/systemd/system/node_exporter.service &lt;&lt;EOF
[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/<span class="hljs-built_in">local</span>/bin/node_exporter

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl start node_exporter
sudo systemctl <span class="hljs-built_in">enable</span> node_exporter
sudo systemctl status node_exporter

<span class="hljs-comment"># Get Tailscale IP for Prometheus config</span>
tailscale ip -4
</code></pre>
<h3 id="heading-3-update-prometheus-config-with-tailscale-ips">3. Update Prometheus Config with Tailscale IPs</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># On ct-monitor, get Tailscale IPs from EC2 instances</span>
<span class="hljs-comment"># Then edit Prometheus config</span>
nano /etc/prometheus/prometheus.yml

<span class="hljs-comment"># Replace BACKEND_TAILSCALE_IP and FRONTEND_TAILSCALE_IP with actual IPs</span>
<span class="hljs-comment"># Example:</span>
<span class="hljs-comment">#   - targets: ['100.117.212.126:9100']  # Backend</span>
<span class="hljs-comment">#   - targets: ['100.124.182.20:9100']   # Frontend</span>

<span class="hljs-comment"># Restart Prometheus</span>
systemctl restart prometheus
</code></pre>
<h3 id="heading-4-access-grafana">4. Access Grafana</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Open browser</span>
http://192.168.8.102:3000

<span class="hljs-comment"># Login: admin / admin</span>
<span class="hljs-comment"># Change password when prompted</span>
</code></pre>
<h3 id="heading-5-configure-grafana-data-source">5. Configure Grafana Data Source</h3>
<ol>
<li><h3 id="heading-login-to-grafana">Login to Grafana</h3>
</li>
<li><p>Go to <strong>Configuration</strong> → <strong>Data Sources</strong></p>
</li>
<li><p>Click <strong>Add data source</strong></p>
</li>
<li><p>Select <strong>Prometheus</strong></p>
</li>
<li><p>URL: <a target="_blank" href="http://localhost:9090"><code>http://localhost:9090</code></a></p>
</li>
<li><p>Click <strong>Save &amp; Test</strong></p>
</li>
</ol>
<h3 id="heading-6-import-dashboard">6. Import Dashboard</h3>
<ol>
<li><p>Go to <strong>Dashboards</strong> → <strong>Import</strong></p>
</li>
<li><p>Enter ID: <code>1860</code> (Node Exporter Full)</p>
</li>
<li><p>Click <strong>Load</strong></p>
</li>
<li><p>Select Prometheus data source</p>
</li>
<li><p>Click <strong>Import</strong></p>
</li>
</ol>
<h3 id="heading-verification-commands">Verification Commands</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Check all services on ct-monitor</span>
systemctl status prometheus
systemctl status node_exporter
systemctl status grafana-server

<span class="hljs-comment"># Check ports</span>
ss -tulpn | grep -E <span class="hljs-string">'9090|9100|3000'</span>

<span class="hljs-comment"># Test from Backend EC2</span>
curl http://192.168.8.102:9090
curl http://192.168.8.102:3000
</code></pre>
<hr />
<h2 id="heading-proxmox-host-ct-connectivity-checks">Proxmox host ↔ CT connectivity checks</h2>
<h4 id="heading-step-1-check-container-network-interface-run-on-proxmox-host">Step 1 — Check container network interface (run on Proxmox host)</h4>
<pre><code class="lang-bash">pct <span class="hljs-built_in">exec</span> 101 -- ip addr
</code></pre>
<p>This shows the container’s eth0 IP and whether the interface is UP. If eth0 is missing or has no IP, it cannot respond.</p>
<h4 id="heading-step-2-check-container-routing">Step 2 — Check container routing</h4>
<pre><code class="lang-bash">pct <span class="hljs-built_in">exec</span> 101 -- ip route
</code></pre>
<p>You should see something like:</p>
<pre><code class="lang-text">default via 192.168.8.1 dev eth0
192.168.8.0/24 dev eth0 proto kernel scope link src 192.168.8.101
</code></pre>
<h4 id="heading-step-3-check-bridge-connectivity-on-host">Step 3 — Check bridge connectivity on host</h4>
<pre><code class="lang-bash">brctl show vmbr0
ip addr show vmbr0
</code></pre>
<p>Confirm vmbr0 is UP and the container veth (veth101i0) is attached.</p>
<h4 id="heading-step-4-restart-container-network-stack">Step 4 — Restart container network stack</h4>
<p>Sometimes the network inside a container doesn’t come up automatically after stop/start:</p>
<pre><code class="lang-bash">pct <span class="hljs-built_in">exec</span> 101 -- ip link <span class="hljs-built_in">set</span> eth0 up
pct <span class="hljs-built_in">exec</span> 101 -- ip addr add 192.168.8.101/24 dev eth0
pct <span class="hljs-built_in">exec</span> 101 -- ip route add default via 192.168.8.1
</code></pre>
<p>Adjust IP/gateway as per your container config.</p>
<p>Now test:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># From Proxmox host</span>
ping 192.168.8.101 -c 3

<span class="hljs-comment"># Inside container</span>
ping 8.8.8.8 -c 3
</code></pre>
<hr />
<h2 id="heading-backend-ec2-ct-db-over-tailscale">Backend EC2 ↔ ct-db over Tailscale</h2>
<p>On Proxmox host (advertise the LXC subnet):</p>
<pre><code class="lang-bash">curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --advertise-routes=192.168.8.0/24
</code></pre>
<p>On EC2 (accept advertised routes):</p>
<pre><code class="lang-bash">curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-routes
</code></pre>
<p>On backend EC2, validate routing and DB port reachability:</p>
<pre><code class="lang-bash">tailscale status
ip route get 192.168.8.101   <span class="hljs-comment"># expect dev tailscale0</span>
nc -zv 192.168.8.101 3306    <span class="hljs-comment"># DB port test (MariaDB/MySQL)</span>
</code></pre>
<p>Troubleshooting (see Article 1): ensure IP forwarding + NAT on Proxmox, and <code>--accept-routes</code> on EC2.</p>
<hr />
<h2 id="heading-monitoring-on-ct-monitor-prometheus-grafana">Monitoring on ct-monitor (Prometheus + Grafana)</h2>
<p>On ct-monitor:</p>
<ol>
<li><p>Install Prometheus, run as a dedicated user, and expose on 0.0.0.0:9090</p>
</li>
<li><p>Configure scrape targets (<a target="_blank" href="http://localhost">localhost</a> and any exporters)</p>
</li>
<li><p>Install Grafana via apt repo; enable grafana-server</p>
</li>
<li><p>Access Grafana from your LAN and add dashboards (system metrics, DB metrics, blackbox checks)</p>
</li>
</ol>
<p>Your notes include working unit files and a minimal prometheus.yml; reuse them here.</p>
<hr />
<h2 id="heading-end-to-end-test-and-verification">End-to-end test and verification</h2>
<ol>
<li><p>From frontend: open the site via public IP; navigate to the registration page</p>
</li>
<li><p>Submit a username/password; the request hits frontend <code>/api/register</code> and proxies to backend private IP:3000</p>
</li>
<li><p>Backend connects to ct-db at 192.168.8.101:3306 (over Tailscale) and inserts into users</p>
</li>
<li><p>Confirm in DB: SELECT * FROM users; on ct-db</p>
</li>
<li><p>Optional: Create a Grafana dashboard to visualize backend host metrics</p>
</li>
</ol>
<p>If requests time out or DB connection fails, check Article 1’s troubleshooting section (routing, NAT, bind-address, user grants).</p>
]]></content:encoded></item><item><title><![CDATA[Understanding nginx: Web Server, Reverse Proxy, and Load Balancer Explained with Docker Compose]]></title><description><![CDATA[Modern web applications need to handle thousands of concurrent users while maintaining high performance and reliability. This is where nginx shines as one of the most powerful and versatile web servers available today. In this comprehensive guide, we...]]></description><link>https://blog.sachindu.me/understanding-nginx-web-server-reverse-proxy-and-load-balancer-explained-with-docker-compose</link><guid isPermaLink="true">https://blog.sachindu.me/understanding-nginx-web-server-reverse-proxy-and-load-balancer-explained-with-docker-compose</guid><category><![CDATA[nginx]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[Load Balancing]]></category><category><![CDATA[Reverse Proxy]]></category><category><![CDATA[webserver]]></category><category><![CDATA[apache]]></category><category><![CDATA[http]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Devops articles]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Wed, 06 Aug 2025 17:54:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754502458081/f2c0a460-ed34-4b4c-a731-3cf8845783e0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754498187143/7d2cd8e2-39ab-4b96-b73f-cf0485ca56c3.png" alt class="image--center mx-auto" /></p>
<p>Modern web applications need to handle thousands of concurrent users while maintaining high performance and reliability. This is where nginx shines as one of the most powerful and versatile web servers available today. In this comprehensive guide, we'll explore how nginx functions as a web server, reverse proxy, and load balancer through a hands-on Docker example.</p>
<p><mark>GitHub Repository: </mark> <a target="_blank" href="https://github.com/sachindumalshan/simple-nginx-app.git"><mark>https://github.com/sachindumalshan/simple-nginx-app.git</mark></a></p>
<hr />
<h2 id="heading-what-is-nginx">What is nginx?</h2>
<p>nginx (pronounced "engine-x") is a high-performance web server, reverse proxy server, and load balancer. Nginx has become one of the most popular web servers in the world, powering over 30% of all websites globally.</p>
<h3 id="heading-key-features-of-nginx">Key Features of Nginx:</h3>
<ul>
<li><p><strong>High Performance</strong>: Can handle thousands of concurrent connections with low memory usage</p>
</li>
<li><p><strong>Reverse Proxy</strong>: Routes client requests to backend servers</p>
</li>
<li><p><strong>Load Balancing</strong>: Distributes incoming requests across multiple servers</p>
</li>
<li><p><strong>Static Content Serving</strong>: Efficiently serves static files like HTML, CSS, and images</p>
</li>
<li><p><strong>SSL/TLS Termination</strong>: Handles encryption and decryption</p>
</li>
<li><p><strong>Caching</strong>: Improves performance by storing frequently requested content</p>
</li>
</ul>
<h2 id="heading-demo-application-architecture">Demo Application Architecture</h2>
<p>Before diving into concepts, let's understand a practical example:</p>
<pre><code class="lang-bash">┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Browser   │────│    Nginx    │────│  Backend 1  │
│             │    │  (Port 8080)│    │   (PHP)     │
└─────────────┘    │             │    └─────────────┘
                   │             │    
                   │             │    ┌─────────────┐
                   │             │────│  Backend 2  │
                   └─────────────┘    │   (PHP)     │
                                      └─────────────┘
</code></pre>
<p>The application consists of:</p>
<ul>
<li><p><strong>Frontend</strong>: Static HTML contact form served by nginx</p>
</li>
<li><p><strong>Two PHP Back-ends</strong>: Process form submissions and show container IDs with a message</p>
</li>
<li><p><strong>nginx</strong>: Acts as web server, reverse proxy, and load balancer</p>
</li>
<li><p><strong>Docker</strong>: Orchestrates all services</p>
</li>
</ul>
<hr />
<h2 id="heading-nginx-as-a-web-server">Nginx as a Web Server</h2>
<h3 id="heading-what-is-a-web-server">What is a Web Server?</h3>
<p>A web server is software that serves web content to clients (browsers) over HTTP/HTTPS. When you type a URL in your browser, you're making a request to a web server.</p>
<h3 id="heading-how-nginx-works-as-a-web-server">How nginx Works as a Web Server</h3>
<p>In this example, nginx serves the static HTML contact form directly to users:</p>
<pre><code class="lang-nginx"><span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;

    <span class="hljs-attribute">location</span> / {
        <span class="hljs-attribute">root</span> /usr/share/nginx/html;
        <span class="hljs-attribute">index</span> index.html;
    }
}
</code></pre>
<p><strong>Key Points:</strong></p>
<ul>
<li><p>Nginx listens on port 80 for incoming HTTP requests</p>
</li>
<li><p>Static files (HTML, CSS, JS) are served directly from <code>/usr/share/nginx/html</code></p>
</li>
<li><p>Clean, minimal configuration for serving static content</p>
</li>
</ul>
<h3 id="heading-advantages-of-nginx-as-a-web-server">Advantages of Nginx as a Web Server:</h3>
<ul>
<li><p><strong>Speed</strong>: Handles static content incredibly fast</p>
</li>
<li><p><strong>Low Memory Usage</strong>: Uses an event-driven architecture</p>
</li>
<li><p><strong>Concurrent Connections</strong>: Can handle thousands of simultaneous connections</p>
</li>
<li><p><strong>Compression</strong>: Built-in gzip compression reduces bandwidth usage</p>
</li>
</ul>
<hr />
<h2 id="heading-nginx-as-a-reverse-proxy">Nginx as a Reverse Proxy</h2>
<h3 id="heading-what-is-a-reverse-proxy">What is a Reverse Proxy?</h3>
<p>A reverse proxy sits between clients and servers, forwarding client requests to appropriate backend servers and then returning the server's response back to the client. Unlike a forward proxy (which acts on behalf of clients), a reverse proxy acts on behalf of servers.</p>
<h3 id="heading-how-reverse-proxy-use-in-this-example">How reverse proxy use in this example</h3>
<p>When submit the contact form, nginx forwards the request to our PHP back-ends:</p>
<pre><code class="lang-nginx"><span class="hljs-comment"># Proxy API requests to backend servers</span>
<span class="hljs-attribute">location</span> /api/ {
    <span class="hljs-comment"># Remove /api prefix when forwarding to backend</span>
    <span class="hljs-attribute">rewrite</span><span class="hljs-regexp"> ^/api/(.*)$</span> /<span class="hljs-variable">$1</span> <span class="hljs-literal">break</span>;

    <span class="hljs-comment"># Proxy to upstream backend servers</span>
    <span class="hljs-attribute">proxy_pass</span> http://backend;

    <span class="hljs-comment"># Headers for proper proxying</span>
    <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
    <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
    <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
    <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
}
</code></pre>
<p><strong>How it works:</strong></p>
<ol>
<li><p>Browser submits form to <code>/api/contact.php</code></p>
</li>
<li><p>nginx receives the request</p>
</li>
<li><p>nginx removes <code>/api</code> prefix and forwards to backend</p>
</li>
<li><p>Backend processes the request</p>
</li>
<li><p>nginx returns the response to the browser</p>
</li>
</ol>
<h3 id="heading-benefits-of-reverse-proxy">Benefits of Reverse Proxy:</h3>
<ul>
<li><p><strong>Security</strong>: Hides backend server details from clients</p>
</li>
<li><p><strong>SSL Termination</strong>: Handles encryption/decryption</p>
</li>
<li><p><strong>Caching</strong>: Can cache responses to improve performance</p>
</li>
<li><p><strong>Compression</strong>: Compresses responses before sending to clients</p>
</li>
<li><p><strong>Request Routing</strong>: Routes requests based on various criteria</p>
</li>
</ul>
<hr />
<h2 id="heading-nginx-as-a-load-balancer">Nginx as a Load Balancer</h2>
<h3 id="heading-what-is-load-balancing">What is Load Balancing?</h3>
<p>Load balancing distributes incoming network traffic across multiple servers to ensure no single server becomes overwhelmed. This improves application responsiveness and availability.</p>
<h3 id="heading-load-balancing-in-this-example">Load balancing in this example</h3>
<p>Our Nginx configuration uses upstream servers for load balancing:</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">upstream</span> backend_servers {
    <span class="hljs-attribute">server</span> backend1:<span class="hljs-number">80</span>;
    <span class="hljs-attribute">server</span> backend2:<span class="hljs-number">80</span>;
    <span class="hljs-comment"># Explicitly use round-robin (default, but let's be clear)</span>
}
</code></pre>
<p><strong>Critical Configuration for Effective Load Balancing:</strong></p>
<pre><code class="lang-nginx"><span class="hljs-attribute">location</span> /api/ {
    <span class="hljs-attribute">proxy_pass</span> http://backend_servers/;
    <span class="hljs-comment"># These settings ensure proper load balancing</span>
    <span class="hljs-attribute">proxy_http_version</span> <span class="hljs-number">1</span>.<span class="hljs-number">1</span>;
    <span class="hljs-attribute">proxy_set_header</span> Connection <span class="hljs-string">""</span>;
}8
</code></pre>
<p><strong>Why these settings matter:</strong></p>
<ul>
<li><p><code>proxy_http_version 1.1</code>: Forces HTTP/1.1 protocol</p>
</li>
<li><p><code>proxy_set_header Connection ""</code>: Prevents connection reuse</p>
</li>
<li><p><strong>Result</strong>: Each request gets a fresh connection, ensuring visible round-robin distribution</p>
</li>
</ul>
<h3 id="heading-load-balancing-methods">Load Balancing Methods:</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754503543331/479cec84-ad4e-4fb1-9839-7483bbfc4c75.jpeg" alt class="image--center mx-auto" /></p>
<ol>
<li><p><strong>Round Robin</strong> (this example): Requests are distributed sequentially</p>
</li>
<li><p><strong>Least Connections</strong>: Routes to server with fewest active connections</p>
</li>
<li><p><strong>IP Hash</strong>: Routes based on client IP hash</p>
</li>
<li><p><strong>Weighted</strong>: Assigns different weights to servers</p>
</li>
</ol>
<p><mark>More info: </mark> <a target="_blank" href="https://www.geeksforgeeks.org/system-design/load-balancing-algorithms/"><mark>https://www.geeksforgeeks.org/system-design/load-balancing-algorithms/</mark></a></p>
<h3 id="heading-demonstrating-load-balancing">Demonstrating Load Balancing</h3>
<p>When you submit forms in our application, you'll notice:</p>
<ul>
<li><p>First submission → Backend 1 (shows container ID)</p>
</li>
<li><p>Second submission → Backend 2 (shows different container ID)</p>
</li>
<li><p>Third submission → Backend 1 (cycle repeats)</p>
</li>
</ul>
<p>This demonstrates round-robin load balancing in action!</p>
<hr />
<h2 id="heading-apache-vs-nginx-backend-processing">Apache vs nginx: Backend Processing</h2>
<h3 id="heading-apache-web-server">Apache Web Server</h3>
<p>Apache uses a process-based or thread-based approach:</p>
<pre><code class="lang-bash">Apache Architecture:
┌─────────────┐
│   Request   │
└─────┬───────┘
      │
┌─────▼───────┐
│   Process/  │ ← Each request gets its own process/thread
│   Thread    │
└─────┬───────┘
      │
┌─────▼───────┐
│   PHP       │ ← PHP processes the request
│  Handler    │
└─────────────┘
</code></pre>
<p><strong>Apache Characteristics:</strong></p>
<ul>
<li><p><strong>Process/Thread per connection</strong>: More memory usage</p>
</li>
<li><p><strong>Synchronous processing</strong>: Blocks while waiting for I/O</p>
</li>
<li><p><strong>Easier configuration</strong>: More straightforward for beginners</p>
</li>
<li><p><strong>Module system</strong>: Extensive module ecosystem</p>
</li>
</ul>
<h3 id="heading-how-our-php-back-ends-work-with-apache">How Our PHP Back-ends Work with Apache</h3>
<p>In the Docker setup, each backend container runs Apache with PHP:</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Our backends use php:8.1-apache image which includes:</span>
<span class="hljs-comment"># - Apache web server</span>
<span class="hljs-comment"># - PHP interpreter</span>
<span class="hljs-comment"># - mod_php module for Apache</span>
</code></pre>
<p>The PHP code processes form data:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>
<span class="hljs-keyword">if</span> ($_SERVER[<span class="hljs-string">'REQUEST_METHOD'</span>] === <span class="hljs-string">'POST'</span>) {
    $name = htmlspecialchars($_POST[<span class="hljs-string">'name'</span>] ?? <span class="hljs-string">''</span>);
    $email = htmlspecialchars($_POST[<span class="hljs-string">'email'</span>] ?? <span class="hljs-string">''</span>);
    $server = gethostname(); <span class="hljs-comment">// Shows which container processed it</span>

    <span class="hljs-comment">// Clean, inline HTML response with styling</span>
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;div style='font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 30px; background: #f8f9fa; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);'&gt;"</span>;
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;h2 style='color: #28a745; text-align: center;'&gt;✓ Message Sent Successfully!&lt;/h2&gt;"</span>;
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;p&gt;&lt;strong&gt;Name:&lt;/strong&gt; <span class="hljs-subst">$name</span>&lt;/p&gt;"</span>;
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;p&gt;&lt;strong&gt;Email:&lt;/strong&gt; <span class="hljs-subst">$email</span>&lt;/p&gt;"</span>;
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;p style='font-size: 12px; color: #666; text-align: center; margin-top: 20px;'&gt;Processed by Backend Server: <span class="hljs-subst">$server</span>&lt;/p&gt;"</span>;
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/div&gt;"</span>;
}
<span class="hljs-meta">?&gt;</span>
</code></pre>
<h3 id="heading-nginx-vs-apache-performance">Nginx vs Apache Performance</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Aspect</td><td>Nginx</td><td>Apache</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Architecture</strong></td><td>Event-driven, asynchronous</td><td>Process/thread-based</td></tr>
<tr>
<td><strong>Memory Usage</strong></td><td>Low, constant</td><td>Higher, scales with connections</td></tr>
<tr>
<td><strong>Static Content</strong></td><td>Excellent</td><td>Good</td></tr>
<tr>
<td><strong>Dynamic Content</strong></td><td>Requires backend (PHP-FPM)</td><td>Built-in (mod_php)</td></tr>
<tr>
<td><strong>Configuration</strong></td><td>Simpler syntax</td><td>More complex but flexible</td></tr>
<tr>
<td><strong>Modules</strong></td><td>Compiled-in</td><td>Dynamic loading</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-running-the-demo-application">Running the Demo Application</h2>
<h3 id="heading-prerequisites">Prerequisites</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Install Docker and Docker Compose</span>
sudo apt update
sudo apt install docker.io docker-compose

<span class="hljs-comment"># Add user to docker group</span>
sudo usermod -aG docker <span class="hljs-variable">$USER</span>
</code></pre>
<h3 id="heading-setup-and-run">Setup and Run</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Clone or create the project structure</span>
mkdir simple-nginx-app &amp;&amp; <span class="hljs-built_in">cd</span> simple-nginx-app

<span class="hljs-comment"># Create all files as shown in the first artifact</span>
<span class="hljs-comment"># Then run:</span>
docker-compose up -d

<span class="hljs-comment"># Access at: http://localhost:8080</span>
</code></pre>
<h3 id="heading-testing-load-balancing">Testing Load Balancing</h3>
<ol>
<li><p>Open <a target="_blank" href="http://localhost:8080"><code>http://localhost:8080</code></a></p>
</li>
<li><p>Fill out the contact form</p>
</li>
<li><p>Submit multiple times</p>
</li>
<li><p>Notice how container IDs alternate between submissions</p>
</li>
<li><p>This demonstrates round-robin load balancing!</p>
</li>
</ol>
<hr />
<h2 id="heading-visual-demo-round-robin-load-balancing-in-action">Visual Demo: Round Robin Load Balancing in Action</h2>
<p>Once the application is up and running, can <strong>observe the backend switching behavior using container IDs</strong>.</p>
<h4 id="heading-step-1-check-running-containers">✅ Step 1: Check Running Containers</h4>
<p>Run the following command to view active containers and their IDs:</p>
<pre><code class="lang-bash">docker ps
</code></pre>
<p><em>List of running containers with their names and container IDs</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754499170920/957b65bc-e293-40ab-af45-14667a291e89.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-step-2-first-form-submission">✅ Step 2: First Form Submission</h4>
<p>Open the app in your browser at <a target="_blank" href="http://localhost:8080"><strong>http://localhost:8080</strong></a></p>
<ol>
<li><p>Fill in the contact form with test values (e.g., <code>Alice</code>, <code>alice@example.com</code>)</p>
</li>
<li><p>Click <strong>Submit</strong></p>
</li>
</ol>
<p><em>Filled contact form before submission</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754499245782/42a1f6f7-a69c-4a68-a00e-ba42e6ec4c4e.png" alt class="image--center mx-auto" /></p>
<p><em>Output showing “Message Sent Successfully!” along with a container ID (e.g., backend1)</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754499277594/f1deaea9-70bb-40a3-8b5e-296ba94ea852.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-step-3-second-form-submission">✅ Step 3: Second Form Submission</h4>
<p>Repeat the process with different values (e.g., <code>Bob</code>, <code>bob@example.com</code>):</p>
<p><em>Second form submission</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754499372277/be1bcce8-0d08-43bc-b6df-048a08d0bae5.png" alt class="image--center mx-auto" /></p>
<p><em>Response showing success message with a</em> <strong><em>different</em></strong> <em>container ID (e.g., backend2)</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754499403886/ccbb8942-a014-4067-9d34-ff42f9669979.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-step-4-repeat-to-observe-load-balancing">✅ Step 4: Repeat to Observe Load Balancing</h4>
<p>Each time you submit the form, you'll notice the container ID switches between backend1 and backend2. This confirms the <strong>round robin</strong> load balancing behavior configured in nginx.</p>
<p><em>Demonstration of backend switching between requests</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754499503085/da705daf-2951-48b2-b75c-2d00ff1a2963.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-troubleshooting-common-issues">Troubleshooting Common Issues</h2>
<h3 id="heading-1-502-bad-gateway">1. <strong>502 Bad Gateway</strong></h3>
<ul>
<li><p>Backend servers are down</p>
</li>
<li><p>Network connectivity issues</p>
</li>
<li><p>Check <code>docker-compose logs</code></p>
</li>
</ul>
<h3 id="heading-2-load-balancing-not-working">2. <strong>Load Balancing Not Working</strong></h3>
<ul>
<li><p>Verify upstream configuration</p>
</li>
<li><p>Check backend server status</p>
</li>
<li><p>Review Nginx error logs</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>nginx's versatility as a web server, reverse proxy, and load balancer makes it an essential tool for modern web applications. This Docker example demonstrates these concepts in action:</p>
<ul>
<li><p><strong>Web Server</strong>: Efficiently serves static HTML content</p>
</li>
<li><p><strong>Reverse Proxy</strong>: Routes API requests to backend services</p>
</li>
<li><p><strong>Load Balancer</strong>: Distributes traffic across multiple PHP back-ends using round-robin</p>
</li>
</ul>
<p>The combination of Nginx with containerized back-ends provides a robust, scalable architecture that can handle high traffic while maintaining excellent performance. Whether you're building microservices, implementing high availability, or optimizing web application performance, understanding these concepts is crucial for modern web development.</p>
]]></content:encoded></item><item><title><![CDATA[Deploy Simple Microservices App on K3s with GitLab CI/CD]]></title><description><![CDATA[Introduction
Micro-services architecture is everywhere in modern software development, but getting started with Kubernetes can feel overwhelming. That's where K3s comes in - a lightweight Kubernetes distribution that's perfect for learning and develo...]]></description><link>https://blog.sachindu.me/deploy-simple-microservices-app-on-k3s-with-gitlab-cicd</link><guid isPermaLink="true">https://blog.sachindu.me/deploy-simple-microservices-app-on-k3s-with-gitlab-cicd</guid><category><![CDATA[ AutomatedDeployment]]></category><category><![CDATA[cloud native]]></category><category><![CDATA[container orchestration]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[k3s]]></category><category><![CDATA[GitLab]]></category><category><![CDATA[gitlab-runner]]></category><category><![CDATA[GitLab-CI]]></category><category><![CDATA[Docker]]></category><category><![CDATA[cicd complete proccess]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Fri, 18 Jul 2025 06:22:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752815913944/c4a9d7b3-a27b-47f0-ac19-9ca7499d6207.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Micro-services architecture is everywhere in modern software development, but getting started with Kubernetes can feel overwhelming. That's where K3s comes in - a lightweight Kubernetes distribution that's perfect for learning and development.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752694698239/23cf9422-6125-4f71-bcf8-a99d12c37f6c.gif" alt class="image--center mx-auto" /></p>
<p>In this tutorial, I build and deploy a simple two-service application:</p>
<ul>
<li><p><strong>User Service</strong>: Returns user information</p>
</li>
<li><p><strong>Greeting Service</strong>: Calls the user service and creates personalized greetings</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752865369385/1a85994a-9b76-4b88-88e9-3cdb52d6fb14.jpeg" alt class="image--center mx-auto" /></p>
<p>Learning areas:</p>
<ul>
<li><p>Set up K3s on your server</p>
</li>
<li><p>Containerize Node.js applications with Docker</p>
</li>
<li><p>Deploy services to Kubernetes</p>
</li>
<li><p>Automate deployments with GitLab CI/CD</p>
</li>
</ul>
<p><mark>GitLab Repository</mark>: <a target="_blank" href="https://gitlab.com/sachindu_personal/KubeGreet.git">https://gitlab.com/sachindu_personal/KubeGreet.git</a></p>
<hr />
<h2 id="heading-step-1-prerequisites-and-k3s-installation">✅ Step 1: Prerequisites and K3s Installation</h2>
<h3 id="heading-install-k3s-on-your-server">Install K3s on Your Server</h3>
<p>K3s is a lightweight Kubernetes distribution perfect for development and production environments.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install K3s</span>
curl -sfL https://get.k3s.io | sh -

<span class="hljs-comment"># Check node status (takes ~30 seconds)</span>
sudo k3s kubectl get node

<span class="hljs-comment"># Create kubectl symlink for easier access</span>
sudo ln -s /usr/<span class="hljs-built_in">local</span>/bin/k3s /usr/<span class="hljs-built_in">local</span>/bin/kubectl
</code></pre>
<p><strong>Expected output:</strong></p>
<pre><code class="lang-bash">NAME          STATUS   ROLES                  AGE   VERSION
thinkcentre   Ready    control-plane,master   93s   v1.32.6+k3s1
</code></pre>
<h3 id="heading-uninstall-k3s-if-needed">Uninstall K3s (if needed)</h3>
<pre><code class="lang-bash">/usr/<span class="hljs-built_in">local</span>/bin/k3s-uninstall.sh
</code></pre>
<hr />
<h2 id="heading-step-2-building-the-microservices-application">✅ Step 2: Building the Microservices Application</h2>
<h3 id="heading-project-structure">Project Structure</h3>
<p>Create the following directory structure:</p>
<pre><code class="lang-bash">KubeGreet/
├── user-service/
│   ├── index.js
│   ├── package.json
|   ├── package-lock.json
│   ├── Dockerfile
│   └── k8s/
│       └── user-service-deployment.yaml
├── greeting-service/
│   ├── index.js
│   ├── package.json
|   ├── package-lock.json
│   ├── Dockerfile
│   └── k8s/
│       └── greeting-service-deployment.yaml
├── README.md
├──.gitignore
└──.gitlab-ci.yml
</code></pre>
<h3 id="heading-user-service-setup">User Service Setup</h3>
<ol>
<li><p><strong>Create user-service files</strong> (<code>index.js</code> and <code>package.json</code>)</p>
</li>
<li><p><strong>Test locally:</strong></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Install all required packages listed in package.json</span>
 npm install

 <span class="hljs-comment"># Start the Node.js application</span>
 npm start
</code></pre>
</li>
<li><p><strong>Verify:</strong> Visit <code>http://localhost:3000/user</code></p>
</li>
</ol>
<h3 id="heading-greeting-service-setup">Greeting Service Setup</h3>
<ol>
<li><p><strong>Create greeting-service files</strong> (<code>index.js</code> and <code>package.json</code>)</p>
</li>
<li><p><strong>Important:</strong> For local testing, modify the service URL:</p>
<pre><code class="lang-javascript"> <span class="hljs-comment">// Change from:const response = await axios.get('http://user-service:3000/user');// To:const response = await axios.get('http://localhost:3000/user');</span>
</code></pre>
</li>
<li><p><strong>Test locally:</strong></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Install all required packages listed in package.json</span>
 npm install

 <span class="hljs-comment"># Start the Node.js application</span>
 npm start
</code></pre>
</li>
<li><p><strong>Verify:</strong> Visit <code>http://localhost:3001/greet</code></p>
</li>
</ol>
<hr />
<h2 id="heading-step-3-containerizing-with-docker">✅ Step 3: Containerizing with Docker</h2>
<h3 id="heading-install-docker-if-not-installed">Install Docker (if not installed)</h3>
<pre><code class="lang-bash">curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

<span class="hljs-comment"># Verify installation</span>
docker run hello-world
</code></pre>
<h3 id="heading-build-and-test-docker-images">Build and Test Docker Images</h3>
<p><strong>User Service:</strong></p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> user-service
docker build -t username/user-service:latest .
docker run -d -p 3000:3000 --name testUserservice username/user-service:latest
</code></pre>
<p><strong>Greeting Service:</strong></p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> greeting-service
docker build -t username/greeting-service:latest .
docker run -d -p 3001:3001 --name testGreetingservice username/greeting-service:latest
</code></pre>
<h3 id="heading-push-to-docker-hub">Push to Docker Hub</h3>
<pre><code class="lang-bash">docker push username/user-service:latest
docker push username/greeting-service:latest
</code></pre>
<hr />
<h2 id="heading-step-4-setting-up-gitlab-repository">✅ Step 4: Setting up GitLab Repository</h2>
<h3 id="heading-initialize-git-repository">Initialize Git Repository</h3>
<pre><code class="lang-bash">git init
git remote add origin https://gitlab.com/your-username/KubeGreet.git

<span class="hljs-comment"># Create .gitignore</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"node_modules/"</span> &gt;&gt; .gitignore

git add .
git commit -m <span class="hljs-string">"initial commit"</span>
git push --set-upstream origin main
</code></pre>
<p><strong>GitLab Access Token</strong></p>
<p>Create a personal access token for authentication:</p>
<ul>
<li><strong>Path</strong>: Preferences &gt; Access Token &gt; Personal Access Token</li>
</ul>
<hr />
<h2 id="heading-step-5-creating-kubernetes-manifests">✅ Step 5: Creating Kubernetes Manifests</h2>
<h3 id="heading-what-is-k3s-and-why-use-it">📌 What is K3s and Why Use It?</h3>
<p><strong>K3s</strong> is a lightweight, easy-to-install Kubernetes distribution designed for <strong>development, edge computing, and IoT environments</strong>. It is simpler and consumes fewer resources compared to full Kubernetes, making it perfect for local testing and learning DevOps workflows.</p>
<h3 id="heading-what-is-a-manifest-file">📌 What is a Manifest File?</h3>
<p>A <strong>Kubernetes manifest file</strong> is a YAML file that defines your Kubernetes resources, such as deployments, services, and config maps. It tells Kubernetes <strong>what to deploy and how to configure it</strong>.</p>
<p>For example, a deployment manifest specifies:</p>
<ul>
<li><p>Which Docker image to use</p>
</li>
<li><p>Number of replicas</p>
</li>
<li><p>Ports to expose</p>
</li>
</ul>
<h3 id="heading-manual-deployment-to-k3s-on-localhost">Manual Deployment to K3s on Localhost</h3>
<p>Create deployment files:</p>
<ul>
<li><p><code>user-service/k8s/user-service-deployment.yaml</code></p>
</li>
<li><p><code>greeting-service/k8s/greeting-service-deployment.yaml</code></p>
</li>
</ul>
<p><strong>Apply manifests:</strong></p>
<pre><code class="lang-bash">kubectl apply -f user-service/k8s/user-service-deployment.yaml
kubectl apply -f greeting-service/k8s/greeting-service-deployment.yaml
</code></pre>
<p><strong>Verify deployment:</strong></p>
<pre><code class="lang-bash">kubectl get pods
</code></pre>
<p><strong>Expected output:</strong></p>
<pre><code class="lang-bash">NAME                                READY   STATUS    RESTARTS   AGE
greeting-service-576bbcfd76-zk2dg   1/1     Running   0          5m
user-service-6d5c9c4f79-r2fxl       1/1     Running   0          5m10s
</code></pre>
<h3 id="heading-useful-commands">Useful Commands</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Scale deployment</span>
kubectl scale deployment user-service --replicas=3

<span class="hljs-comment"># Restart service</span>
kubectl rollout restart deployment user-service

<span class="hljs-comment"># Delete pod</span>
kubectl delete pod &lt;pod-name&gt;
</code></pre>
<hr />
<h2 id="heading-step-6-automated-cicd-pipeline-setup">✅ Step 6: Automated CI/CD Pipeline Setup</h2>
<p><strong>Step 1: Install GitLab Runner</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install GitLab Runner</span>
curl -L <span class="hljs-string">"https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh"</span> | sudo bash
sudo apt-get update
sudo apt-get install gitlab-runner

<span class="hljs-comment"># Verify installation</span>
gitlab-runner --version
</code></pre>
<p><strong>Step 2: Register GitLab Runner</strong></p>
<p>Get Project Runner Token:</p>
<ol>
<li><p>Go to your GitLab project</p>
</li>
<li><p>Navigate to <strong>Settings → CI/CD → Runners</strong></p>
</li>
<li><p>Click <strong>"New project runner"</strong></p>
</li>
<li><p>Configure runner settings:</p>
<ul>
<li><p><strong>Tags</strong>: <code>k3s-deploy</code> (or your preferred tag)</p>
</li>
<li><p><strong>Description</strong>: <code>K3s deployment runner</code></p>
</li>
<li><p><strong>Maximum timeout</strong>: <code>3600</code> seconds (1 hour)</p>
</li>
</ul>
</li>
<li><p>Click <strong>"Create runner"</strong></p>
</li>
<li><p>Copy the registration token</p>
</li>
</ol>
<p><strong>Register the Runner:</strong></p>
<pre><code class="lang-bash">sudo gitlab-runner register \
  --url https://gitlab.com \
  --token &lt;project-runner-token&gt; \
  --executor shell
</code></pre>
<p><em>During registration, you'll be prompted:</em></p>
<ol>
<li><p><strong>Description</strong>: Press Enter (uses default)</p>
</li>
<li><p><strong>Tags</strong>: Press Enter (no tags needed)</p>
</li>
<li><p><strong>Maintenance note</strong>: Press Enter (skip)</p>
</li>
<li><p><strong>Executor</strong>: Type <code>shell</code></p>
</li>
</ol>
<p><strong>Step 3: Start GitLab Runner Service</strong></p>
<pre><code class="lang-bash">sudo gitlab-runner start
sudo systemctl <span class="hljs-built_in">enable</span> gitlab-runner
sudo systemctl status gitlab-runner
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752692791015/26523466-f588-47c5-a0f4-efec6029b683.png" alt class="image--center mx-auto" /></p>
<p>Shared runner should stay on the running state, otherwise Docker image will not be created.</p>
<p><strong>Step 4: Configure kubectl Access</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Set up kubectl for current user</span>
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

<span class="hljs-comment"># Test kubectl</span>
kubectl get nodes
</code></pre>
<p><strong>Step 5: Get Base64 Kubeconfig</strong></p>
<pre><code class="lang-bash">cat ~/.kube/config | base64 -w 0
</code></pre>
<p>Copy the output for GitLab CI/CD variables.</p>
<p><strong>Step 6: Configure GitLab CI/CD Variables</strong></p>
<p>Go to <strong>Settings → CI/CD → Variables</strong> and add:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Variable</td><td>Value</td><td>Type</td></tr>
</thead>
<tbody>
<tr>
<td><code>KUBE_CONFIG</code></td><td>Base64 kubeconfig output</td><td>Variable</td></tr>
<tr>
<td><code>CI_REGISTRY_USER</code></td><td>GitLab username</td><td>Variable</td></tr>
<tr>
<td><code>CI_REGISTRY_PASSWORD</code></td><td>Personal access token</td><td>Variable (Masked)</td></tr>
<tr>
<td><code>CI_REGISTRY</code></td><td>registry.gitlab.com</td><td>Variable</td></tr>
</tbody>
</table>
</div><p><strong>Step 7: Create Docker Registry Secret</strong></p>
<pre><code class="lang-bash">kubectl create secret docker-registry gitlab-registry-secret \
  --docker-server=registry.gitlab.com \
  --docker-username=your-username \
  --docker-password=your-token \
  --docker-email=your-email@gmail.com
</code></pre>
<p><strong>Step 8: Fix Runner Permissions</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add gitlab-runner to sudo group</span>
sudo usermod -a -G sudo gitlab-runner

<span class="hljs-comment"># Fix K3s kubeconfig permissions</span>
sudo chmod 644 /etc/rancher/k3s/k3s.yaml

<span class="hljs-comment"># Alternative: Copy config for gitlab-runner</span>
sudo mkdir -p /home/gitlab-runner/.kube
sudo cp /etc/rancher/k3s/k3s.yaml /home/gitlab-runner/.kube/config
sudo chown gitlab-runner:gitlab-runner /home/gitlab-runner/.kube/config
sudo chmod 600 /home/gitlab-runner/.kube/config
</code></pre>
<p><strong>Step 9: Test Deployment</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Test manual deployment</span>
kubectl apply -f user-service/k8s/user-service-deployment.yaml
kubectl get pods -l app=user-service

<span class="hljs-comment"># Test the service</span>
curl http://localhost:3000/greet
</code></pre>
<p><strong>Step 10: Run CI/CD Pipeline</strong></p>
<p>Now let's test the complete CI/CD pipeline by making a commit and verifying the automated deployment.</p>
<p><strong>Trigger the Pipeline</strong></p>
<p>Make a small change to test the automation:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Commit and push</span>
git add .
git commit -m <span class="hljs-string">"Test CI/CD pipeline"</span>
git push origin main
</code></pre>
<p><strong>Monitor Pipeline Execution</strong></p>
<ol>
<li><p><strong>Go to GitLab</strong>: Navigate to your project → <strong>CI/CD → Pipelines</strong></p>
</li>
<li><p><strong>Check Pipeline Status</strong>: You should see a running pipeline with stages:</p>
<ul>
<li><p>✅ <strong>Build</strong>: Docker images are built and pushed</p>
</li>
<li><p>✅ <strong>Deploy</strong>: Services are deployed to K3s cluster</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-verify-successful-deployment">Verify Successful Deployment</h3>
<p><strong>Check Pipeline Success:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check if pods are running</span>
kubectl get pods

<span class="hljs-comment"># Expected output:</span>
NAME                                READY   STATUS    RESTARTS   AGE
greeting-service-576bbcfd76-zk2dg   1/1     Running   0          2m
user-service-6d5c9c4f79-r2fxl       1/1     Running   0          2m
</code></pre>
<p><strong>Test the Application:</strong> Visit in your browser: <code>http://&lt;server-ip&gt;:30081/greet</code></p>
<p><strong>Expected Output:</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752693923275/f62d6d9d-e998-4552-95bb-24b4e02b7e21.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-common-errors-and-troubleshooting">⚠️ Common Errors and Troubleshooting</h2>
<ol>
<li><p><strong>Pod not starting:</strong> Check logs with <code>kubectl logs -l app=service-name</code></p>
</li>
<li><p><strong>Permission denied:</strong> Ensure gitlab-runner has proper permissions</p>
</li>
<li><p><strong>Registry pull errors:</strong> Verify registry secret is created correctly</p>
</li>
</ol>
<p>Useful Commands</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check pod status</span>
kubectl describe pod -l app=user-service

<span class="hljs-comment"># View logs</span>
kubectl logs -l app=user-service

<span class="hljs-comment"># Check services</span>
kubectl get svc
</code></pre>
<hr />
<p>💬 <strong>Tried this flow or need any step explained better? Drop a comment below – I welcome your feedback and let’s grow together as DevOps learners!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Email Subject Generator - Multi-Tier Application Deployment]]></title><description><![CDATA[This project is a Python-based Email Subject Generator Application developed using the Flask framework. It follows a three-tier architecture, consisting of a front-end, back-end API, and database, each running as separate services to ensure modularit...]]></description><link>https://blog.sachindu.me/email-subject-generator-multi-tier-application-deployment</link><guid isPermaLink="true">https://blog.sachindu.me/email-subject-generator-multi-tier-application-deployment</guid><category><![CDATA[Devops]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[CI/CD]]></category><category><![CDATA[Jenkins]]></category><category><![CDATA[ Jenkins, DevOps]]></category><category><![CDATA[Bitbucket]]></category><category><![CDATA[bitbucket-pipelines]]></category><category><![CDATA[Python]]></category><category><![CDATA[Flask Framework]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Sat, 12 Jul 2025 18:43:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752256885627/758a1256-e6fa-4b75-bbc6-92fe117dd0d7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752343628805/ed0b5b75-ddc6-40e9-9a61-581fe62b187e.png" alt class="image--center mx-auto" /></p>
<p>This project is a <strong>Python-based Email Subject Generator Application</strong> developed using the Flask framework. It follows a <strong>three-tier architecture</strong>, consisting of a <strong>front-end, back-end API, and database</strong>, each running as separate services to ensure modularity and scalability.</p>
<p>The tool enables users to input any scenario or context that requires an email. Based on the provided information, the application intelligently generates a <strong>suitable and impactful subject line</strong> for the email, enhancing communication clarity and professionalism.</p>
<p>To streamline <strong>development, testing, and deployment</strong>, we implemented <strong>separate CI and CD pipelines</strong> using <strong>Jenkins</strong>. The source code is hosted on <strong>Bit-bucket</strong>, and every push to the repository triggers an automated CI/CD workflow that:</p>
<ul>
<li><p>Runs <strong>code quality tests</strong> using <strong>SonarQube</strong> to ensure clean, maintainable, and secure code.</p>
</li>
<li><p>Builds and tests the <strong>Flask application</strong> using Jenkins pipelines.</p>
</li>
<li><p>Packages the app into a <strong>Docker image</strong> for consistent deployment across environments.</p>
</li>
<li><p>Pushes the generated Docker image to <strong>Docker Hub</strong>.</p>
</li>
<li><p>Optionally deploys the application to a <strong>target server or cloud platform</strong> via the CD pipeline.</p>
</li>
</ul>
<p><strong><mark>Bit-bucket Repository Link:</mark></strong> <a target="_blank" href="https://sachindumalshan@bitbucket.org/sachindu-work-space/email-subject-generator.git">https://sachindumalshan@bitbucket.org/sachindu-work-space/email-subject-generator.git</a></p>
<hr />
<h2 id="heading-step-1-prerequisites-and-environment-setup">✅ <strong>Step 1: Prerequisites and Environment Setup</strong></h2>
<p>Do the following on the server:</p>
<h3 id="heading-install-docker">I<strong>nstall Docker</strong></h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Install docker original</span>
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

<span class="hljs-comment"># Verify Docker installation:</span>
docker run hello-world
</code></pre>
<h3 id="heading-install-docker-compose"><strong>Install Docker Compose</strong></h3>
<pre><code class="lang-bash">sudo curl -L <span class="hljs-string">"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-<span class="hljs-subst">$(uname -s)</span>-<span class="hljs-subst">$(uname -m)</span>"</span> -o /usr/<span class="hljs-built_in">local</span>/bin/docker-compose

<span class="hljs-comment"># Set the permission to execute Docker Compose:</span>
sudo chmod +x /usr/<span class="hljs-built_in">local</span>/bin/docker-compose

<span class="hljs-comment"># Check Docker Compose version:</span>
docker-compose --version
</code></pre>
<h3 id="heading-native-jenkins-installation">Native Jenkins Installation</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Update package lists and upgrade existing packages</span>
sudo apt update &amp;&amp; sudo apt upgrade -y

<span class="hljs-comment"># Install OpenJDK 17 (required for Jenkins)</span>
sudo apt install openjdk-17-jdk -y

<span class="hljs-comment"># Verify Java installation</span>
java -version
</code></pre>
<p>Download and add the Jenkins GPG key. This command adds Jenkins’ official GPG key to your system’s keyring so that APT trusts Jenkins packages.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Download and add the Jenkins GPG key to the system keyrings</span>
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc &gt; /dev/null
</code></pre>
<p>Update package index and install Jenkins</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Update package index after adding Jenkins repository key</span>
sudo apt update

<span class="hljs-comment"># Install Jenkins</span>
sudo apt install jenkins -y

<span class="hljs-comment"># Check if port 8080 is already in use (Jenkins default port)</span>
sudo netstat -tlnp | grep :8080

<span class="hljs-comment"># Check current firewall status</span>
sudo ufw status

<span class="hljs-comment"># Allow incoming connections on Jenkins port (8080)</span>
sudo ufw allow 8080

<span class="hljs-comment"># Start Jenkins service</span>
sudo systemctl start jenkins

<span class="hljs-comment"># Enable Jenkins to start on boot</span>
sudo systemctl <span class="hljs-built_in">enable</span> jenkins
</code></pre>
<h4 id="heading-access-jenkins-from-another-computer">Access Jenkins from Another Computer</h4>
<ul>
<li>Open the URL below in your browser. It will display the Jenkins startup interface and prompt you to enter the administrator password. Use the following command to retrieve the password, then enter it in the prompt to log into Jenkins.</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># Use 'ifconfig' to get the server IP address</span>
<span class="hljs-comment"># Ex: http://192.168.8.129:8080/</span>

http://&lt;server-ip&gt;:8080
</code></pre>
<p>💡 <strong>To get the Jenkins initial admin password, enter the following command on the Jenkins server:</strong></p>
<pre><code class="lang-bash">sudo cat /var/lib/jenkins/secrets/initialAdminPassword
</code></pre>
<p>If you skip user configuration during setup:</p>
<ul>
<li><p><strong>Username:</strong> admin</p>
</li>
<li><p><strong>Password:</strong> Use the output from above command (e.g. a5bba1b0c60d4edaadf420882fb12060)</p>
</li>
</ul>
<h3 id="heading-add-jenkins-user-to-docker-group-and-verify-access"><strong>Add Jenkins User to Docker Group and Verify Access</strong></h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Add the Jenkins user to the Docker group to allow it to run Docker commands</span>
sudo usermod -aG docker jenkins

<span class="hljs-comment"># Restart Jenkins to apply the new group permissions</span>
sudo systemctl restart jenkins

<span class="hljs-comment"># Verify that the Jenkins user can run Docker commands without permission errors</span>
sudo -u jenkins docker ps
</code></pre>
<hr />
<h2 id="heading-step-2-prepare-and-develop-the-application">✅ <strong>Step 2:</strong> Prepare and <strong>Develop the Application</strong></h2>
<p>Project folder structure is here.</p>
<pre><code class="lang-plaintext">email-subject-generator/
├── docker-compose.yml
├── .env
├── .gitignore
├── README.md
├── sonar-project.properties
│
├── frontend/
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── app.py
│   ├── test_app.py
│   ├── templates/
│   │   └── index.html
│   └── static/
│       └── style.css
│
├── backend/
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── app.py
│   └── test_app.py
│
└── database/
    └── init.sql
</code></pre>
<h3 id="heading-1-create-and-test-the-frontend">📝 <strong>1. Create and Test the Frontend</strong></h3>
<p>Navigate to the <strong>frontend/</strong> folder containing:</p>
<ul>
<li><p>Dockerfile</p>
</li>
<li><p>requirements.txt</p>
</li>
<li><p>app.py</p>
</li>
<li><p>test_app.py</p>
</li>
<li><p>templates/index.html</p>
</li>
<li><p>static/style.css</p>
</li>
</ul>
<blockquote>
<p><mark>All the coding files are available in the repository.</mark></p>
</blockquote>
<p>Install all dependencies:</p>
<pre><code class="lang-bash">pip install -r requirements.txt
</code></pre>
<p>To test locally, run:</p>
<pre><code class="lang-bash">python3 app.py
</code></pre>
<p>✔️ When running successfully, it will be available at:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Paste the URL in the broswer</span>
http://0.0.0.0:5000
</code></pre>
<hr />
<h3 id="heading-2-create-the-env-file">📝 <strong>2. Create the .env File</strong></h3>
<pre><code class="lang-plaintext">email-subject-generator/
├── .env
</code></pre>
<p>The <code>.env</code> file is used to store <strong>environment-specific configuration details</strong> such as database credentials, API keys, ports, and other sensitive information required by the application.</p>
<ul>
<li><p><strong>Why use a</strong> <code>.env</code> file?<br />  It helps keep configuration separate from code, allowing easy changes without modifying source files. This approach improves security and flexibility across different environments (development, testing, production).</p>
</li>
<li><p><strong>What is usually stored in a</strong> <code>.env</code> file?</p>
<ul>
<li><p>Database host, user, password, and database name</p>
</li>
<li><p>Secret keys and tokens (e.g., API keys)</p>
</li>
<li><p>Application-specific settings like ports or debug flags</p>
</li>
</ul>
</li>
<li><p><strong>Why is the</strong> <code>.env</code> file usually excluded from remote repositories?<br />  Because it contains sensitive data that should not be publicly exposed or shared, it is common practice to add <code>.env</code> to <code>.gitignore</code>. This prevents accidental leaks of credentials or secrets and keeps your application secure.</p>
</li>
</ul>
<blockquote>
<p><strong>Note:</strong><br />For learning purposes, a sample <code>.env</code> file has been included in the repository to demonstrate its structure and contents.<br />In real projects, avoid committing your actual <code>.env</code> files with sensitive data to public repositories.</p>
</blockquote>
<hr />
<h3 id="heading-3-create-and-test-the-backend-and-database">📝 <strong>3. Create and Test the Backend and Database</strong></h3>
<p>Navigate to the <strong>backend/</strong> and <strong>database/</strong> folders:</p>
<ul>
<li><p>backend/</p>
<ul>
<li><p>Dockerfile</p>
</li>
<li><p>requirements.txt</p>
</li>
<li><p>app.py</p>
</li>
<li><p>test_app.py</p>
</li>
</ul>
</li>
<li><p>database/</p>
<ul>
<li>init.sql</li>
</ul>
</li>
</ul>
<blockquote>
<p><mark>All the coding files are available in the repository.</mark></p>
</blockquote>
<p>Install all dependencies:</p>
<pre><code class="lang-bash">pip install -r requirements.txt
</code></pre>
<p>To test locally, run:</p>
<pre><code class="lang-bash">python3 app.py
</code></pre>
<p>The backend will run on: <code>http://0.0.0.0:5001</code></p>
<p>✅ <strong>Check if the server is listening</strong></p>
<p>Run:</p>
<pre><code class="lang-bash">netstat -tuln | grep 5001
</code></pre>
<p>✔️ If you see output showing your <strong>Python/Flask process listening on port 5001</strong>, it confirms the server is running.</p>
<p>✅ <strong>Use</strong> <code>curl</code> to test endpoints</p>
<p>Run:</p>
<pre><code class="lang-bash">curl http://localhost:5001/api/health
</code></pre>
<p>✔️ You will see output similar to:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"service"</span>: <span class="hljs-string">"email-subject-generator-backend"</span>,
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"healthy"</span>,
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-07-12T12:11:46.423913"</span>
}
</code></pre>
<p>✔️ This confirms the API is working and returning the expected response.</p>
<hr />
<h2 id="heading-step-3-create-docker-files-and-docker-compose-configuration">✅ <strong>Step 3: Create Docker files and Docker Compose Configuration</strong></h2>
<h3 id="heading-note">💡 <strong>Note:</strong></h3>
<p>As per industry best practices, <strong>a Dockerfile is not needed for the database</strong>. Database containers can be configured directly using Docker Compose.</p>
<h3 id="heading-1-dockerfile-frontend"><strong>1. Dockerfile – Frontend</strong></h3>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Use the official Python 3.10.12 image with Alpine Linux (lightweight)</span>
<span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.10</span>.<span class="hljs-number">12</span>-alpine

<span class="hljs-comment"># Set the working directory inside the container to /app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Copy only requirements.txt first to leverage Docker cache for faster builds when dependencies don't change</span>
<span class="hljs-keyword">COPY</span><span class="bash"> requirements.txt .</span>

<span class="hljs-comment"># Install Python dependencies specified in requirements.txt</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install -r requirements.txt</span>

<span class="hljs-comment"># Copy the entire application code into the container's /app directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-comment"># Expose port 5000 so it can be accessed externally if mapped</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">5000</span>

<span class="hljs-comment"># Specify the default command to run the Flask app when the container starts</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"python3"</span>, <span class="hljs-string">"app.py"</span>]</span>
</code></pre>
<blockquote>
<p><strong>Explanation:</strong> The <code>EXPOSE</code> command is used as good practice to document the port the container listens on.</p>
<p><strong>If the build fails to pull the base image, login to Docker Hub:</strong></p>
</blockquote>
<pre><code class="lang-bash">docker login
</code></pre>
<p>✔️ <strong>Run the frontend container:</strong></p>
<pre><code class="lang-bash">sudo docker run -d -p 5000:5000 --name email-gen-frontend emailgen-app:v1
</code></pre>
<h3 id="heading-2-dockerfile-backend"><strong>2. Dockerfile – Backend</strong></h3>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Use the official Python 3.10.12 image with Alpine Linux (lightweight)</span>
<span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.10</span>.<span class="hljs-number">12</span>-alpine

<span class="hljs-comment"># Set the working directory inside the container to /app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Copy only requirements.txt first to leverage Docker cache for faster builds when dependencies don't change</span>
<span class="hljs-keyword">COPY</span><span class="bash"> requirements.txt .</span>

<span class="hljs-comment"># Install Python dependencies specified in requirements.txt</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install -r requirements.txt</span>

<span class="hljs-comment"># Copy the entire application code into the container's /app directory</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-comment"># Expose port 5001 so it can be accessed externally if mapped</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">5001</span>

<span class="hljs-comment"># Specify the default command to run the Flask app when the container starts</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"python3"</span>, <span class="hljs-string">"app.py"</span>]</span>
</code></pre>
<p>✔️ <strong>Run the backend container:</strong></p>
<pre><code class="lang-bash">sudo docker run -d -p 5001:5001 --name emailgen-be emailgen-backend:v1
</code></pre>
<p>Check running containers:</p>
<pre><code class="lang-bash">docker ps

<span class="hljs-comment"># Or view all containers including stopped ones:</span>
docker ps -a

<span class="hljs-comment"># Example output:</span>
CONTAINER ID   IMAGE                 COMMAND            CREATED          STATUS          PORTS                                       NAMES
3979a0bdfe82   emailgen-backend:v1   <span class="hljs-string">"python3 app.py"</span>   6 seconds ago    Up 6 seconds    0.0.0.0:5001-&gt;5001/tcp, :::5001-&gt;5001/tcp   emailgen-be
a771f531d2f9   emailgen-app:v1       <span class="hljs-string">"python3 app.py"</span>   14 minutes ago   Up 14 minutes   0.0.0.0:5000-&gt;5000/tcp, :::5000-&gt;5000/tcp   email-gen-frontend
</code></pre>
<h3 id="heading-3-create-and-run-docker-composeyml"><strong>3. Create and Run</strong> <code>docker-compose.yml</code></h3>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.9'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">frontend:</span>
    <span class="hljs-comment"># Use build for development, or set FRONTEND_IMAGE and IMAGE_TAG env vars for deployment</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">./frontend</span>
    <span class="hljs-comment"># image: ${FRONTEND_IMAGE:-emailgen-frontend}:${IMAGE_TAG:-latest}</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">emailgen-frontend</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5000:5000"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">backend</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">BACKEND_URL=http://backend:5001</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">emailgen-net</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

  <span class="hljs-attr">backend:</span>
    <span class="hljs-comment"># Use build for development, or set BACKEND_IMAGE and IMAGE_TAG env vars for deployment</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">./backend</span>
    <span class="hljs-comment"># image: ${BACKEND_IMAGE:-emailgen-backend}:${IMAGE_TAG:-latest}</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">emailgen-backend</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5001:5001"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_HOST=db</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_PORT=3306</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_DATABASE=email_generator</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_USER=root</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_PASSWORD=rootpassword123</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">GROQ_API_KEY=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</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">emailgen-net</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

  <span class="hljs-attr">db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">mysql:8.0</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">emailgen-db</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_ROOT_PASSWORD=rootpassword123</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">MYSQL_DATABASE=email_generator</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./database/init.sql:/docker-entrypoint-initdb.d/init.sql</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">mysql_data:/var/lib/mysql</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"3306:3306"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">emailgen-net</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"mysqladmin"</span>, <span class="hljs-string">"ping"</span>, <span class="hljs-string">"-h"</span>, <span class="hljs-string">"localhost"</span>, <span class="hljs-string">"-p=rootpassword123"</span>]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">mysql_data:</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">emailgen-net:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<p>After creating your <code>docker-compose.yml</code> file, you can run all services together with a single command.</p>
<h4 id="heading-build-and-run-in-detached-mode">Build and Run in detached mode:</h4>
<p>To run in the background (detached mode), use:</p>
<pre><code class="lang-bash">docker-compose up -d --build
</code></pre>
<h4 id="heading-check-running-containers"><strong>Check running containers</strong></h4>
<pre><code class="lang-bash">docker ps
</code></pre>
<p>You should see output similar to:</p>
<pre><code class="lang-bash">CONTAINER ID   IMAGE                 COMMAND            CREATED          STATUS          PORTS                                       NAMES
abcd1234efgh   emailgen-backend      <span class="hljs-string">"python3 app.py"</span>   5 seconds ago    Up 5 seconds    0.0.0.0:5001-&gt;5001/tcp                     emailgen-backend
ijkl5678mnop   emailgen-frontend     <span class="hljs-string">"python3 app.py"</span>   5 seconds ago    Up 5 seconds    0.0.0.0:5000-&gt;5000/tcp                     emailgen-frontend
qrst9012uvwx   mysql:8.0             <span class="hljs-string">"docker-entrypoi…"</span> 5 seconds ago    Up 5 seconds    33060/tcp, 0.0.0.0:3306-&gt;3306/tcp          emailgen-db
</code></pre>
<p>Here you can see <strong>frontend, backend, and database containers</strong> running with their respective ports.</p>
<h4 id="heading-test-your-application"><strong>Test your application</strong></h4>
<ul>
<li><p>Open your browser and navigate to:</p>
<ul>
<li><p><strong>Frontend:</strong> http://localhost:5000</p>
</li>
<li><p><strong>Backend API:</strong> http://localhost:5001</p>
</li>
</ul>
</li>
</ul>
<p>Confirm that your frontend connects with the backend and data saves/retrieves from the database successfully.</p>
<p>When done testing:</p>
<pre><code class="lang-bash">docker-compose down
</code></pre>
<p>This stops and removes containers, networks, and default volumes created by <code>docker-compose up</code>.</p>
<hr />
<h2 id="heading-step-4-push-code-to-bit-bucket-repository">✅ <strong>Step 4: Push Code to Bit bucket Repository</strong></h2>
<pre><code class="lang-bash"><span class="hljs-comment"># Initialize Git in your local project folder</span>
git init

<span class="hljs-comment"># Create .gitignore file to exclude node_modules and .env for security and clean repo</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"node_modules/"</span> &gt;&gt; .gitignore
<span class="hljs-built_in">echo</span> <span class="hljs-string">".env"</span> &gt;&gt; .gitignore

<span class="hljs-comment"># Track all changes and commit</span>
git add .
git commit -m <span class="hljs-string">"Initial commit"</span>

<span class="hljs-comment"># Connect local repo to GitHub remote (initial setup)</span>
git remote add origin https://sachindumalshan@bitbucket.org/sachindu-work-space/email-subject-generator.git

<span class="hljs-comment"># Push code to main branch on GitHub</span>
git branch -M main
git push -u origin main
</code></pre>
<blockquote>
<p>❗ <strong>Bit bucket Push Error:</strong> Instead of <strong>Bit bucket</strong> password, use <strong>Access Token</strong>.</p>
</blockquote>
<p>Create one from:<br /><a target="_blank" href="https://bitbucket.org/account/settings/app-passwords/">https://bitbucket.org/account/settings/app-passwords/</a> → Generate classic token with full access.</p>
<hr />
<h2 id="heading-step-5-set-up-jenkins">✅ <strong>Step 5: Set Up Jenkins</strong></h2>
<p>Go to: <code>Manage Jenkins &gt; Manage Plugins</code>, Search and install the plugins as needed. Install essential plugins such as:</p>
<ul>
<li><p>Pipeline</p>
</li>
<li><p>Git plugin</p>
</li>
<li><p>Bitbucket Branch Source plugin</p>
</li>
<li><p>SonarQube Scanner plugin</p>
</li>
<li><p>Docker Pipeline plugin</p>
</li>
<li><p>Parameterized Trigger plugin</p>
</li>
<li><p>Pipeline Stage View Plugin</p>
</li>
</ul>
<blockquote>
<p>Ensure these plugins are installed and up-to-date before running the pipelines.</p>
</blockquote>
<h3 id="heading-configure-build-tools"><mark>Configure Build Tools</mark></h3>
<h6 id="heading-1-jdk-configure">1. JDK configure</h6>
<pre><code class="lang-bash"><span class="hljs-comment"># Name</span>
JDK-17

<span class="hljs-comment"># Set the JDK path as:</span>
/usr/lib/jvm/java-17-openjdk-amd64

<span class="hljs-comment"># check java jdk installation path</span>
sudo update-alternatives --config java
</code></pre>
<p>If not found, Update and install OpenJDK 17:</p>
<pre><code class="lang-bash">sudo apt update
sudo apt install openjdk-17-jdk -y
</code></pre>
<h6 id="heading-2-git-configure">2. Git configure</h6>
<pre><code class="lang-bash"><span class="hljs-comment"># Name</span>
Default 

<span class="hljs-comment"># Set the Git path as:</span>
/usr/bin/git

<span class="hljs-comment"># To check git path</span>
<span class="hljs-built_in">which</span> git
</code></pre>
<h6 id="heading-3-docker-configure">3. Docker configure</h6>
<pre><code class="lang-bash"><span class="hljs-comment"># Name</span>
Docker

<span class="hljs-comment"># Set the JDK path as:</span>
/usr/bin/docker
</code></pre>
<hr />
<h3 id="heading-install-and-configure-sonarqube-code-quality-analysis">I<strong>nstall and Configure SonarQube (Code Quality Analysis)</strong></h3>
<p>To run <strong>SonarQube analysis locally</strong>, you'll need to install both:</p>
<ul>
<li><p><strong>SonarQube Server:</strong> The main server that processes and stores analysis results</p>
</li>
<li><p><strong>SonarQube Scanner:</strong> The client tool that analyzes your code and sends results to the server</p>
</li>
</ul>
<p>Below is the <strong>installation guide</strong> for each.</p>
<h3 id="heading-why-use-sonarqube"><strong>Why use SonarQube?</strong></h3>
<ul>
<li><p>Ensures <strong>code quality, security, and maintainability</strong> by automatic static code analysis</p>
</li>
<li><p>Detects <strong>bugs, vulnerabilities, code smells, and duplications</strong> early in the CI/CD pipeline</p>
</li>
<li><p>Provides a <strong>dashboard for project code health</strong> with historical trends and actionable insights</p>
</li>
</ul>
<h3 id="heading-1-install-sonarqube-server">1. Install SonarQube Server</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. Update system packages</span>
sudo apt update

<span class="hljs-comment"># 2. Install Java 17 (required for SonarQube)</span>
sudo apt install -y openjdk-17-jdk

<span class="hljs-comment"># 3. Verify Java installation</span>
java -version

<span class="hljs-comment"># 4. Create SonarQube user (recommended for security)</span>
sudo useradd -r -s /bin/<span class="hljs-literal">false</span> sonarqube

<span class="hljs-comment"># 5. Download SonarQube Community Edition</span>
<span class="hljs-built_in">cd</span> /opt
sudo wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-10.4.1.88267.zip

<span class="hljs-comment"># 6. Extract SonarQube</span>
sudo unzip sonarqube-10.4.1.88267.zip
sudo mv sonarqube-10.4.1.88267 sonarqube
sudo chown -R sonarqube:sonarqube /opt/sonarqube

<span class="hljs-comment"># 7. Configure SonarQube (optional - edit if needed)</span>
sudo nano /opt/sonarqube/conf/sonar.properties

<span class="hljs-comment"># 8. Create systemd service file</span>
sudo tee /etc/systemd/system/sonarqube.service &gt; /dev/null &lt;&lt;EOF
[Unit]
Description=SonarQube service
After=syslog.target network.target

[Service]
Type=forking
ExecStart=/opt/sonarqube/bin/linux-x86-64/sonar.sh start
ExecStop=/opt/sonarqube/bin/linux-x86-64/sonar.sh stop
User=sonarqube
Group=sonarqube
Restart=always
LimitNOFILE=65536
LimitNPROC=4096

[Install]
WantedBy=multi-user.target
EOF

<span class="hljs-comment"># 9. Reload systemd and enable SonarQube service</span>
sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> sonarqube
sudo systemctl start sonarqube

<span class="hljs-comment"># 10. Check SonarQube service status</span>
sudo systemctl status sonarqube

<span class="hljs-comment"># 11. Check if SonarQube is running (may take a few minutes to start)</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"SonarQube is starting... Please wait 5-10 minutes."</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Access SonarQube at: http://localhost:9000"</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Default credentials: admin / admin"</span>
</code></pre>
<h3 id="heading-2-check-sonarqube-status">2. Check SonarQube Status</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Check SonarQube service status</span>
sudo systemctl status sonarqube

<span class="hljs-comment"># Check running processes</span>
ps -ef | grep sonarqube

<span class="hljs-comment"># Check logs for startup or error details</span>
sudo tail -f /opt/sonarqube/logs/sonar.log
</code></pre>
<h3 id="heading-3-troubleshooting-common-sonarqube-issues">3. Troubleshooting Common SonarQube Issues</h3>
<p>If SonarQube is not working, run the script below to diagnose and resolve:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># ===========================================</span>
<span class="hljs-comment"># Common SonarQube Issues and Fixes</span>
<span class="hljs-comment"># ===========================================</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 1: If SonarQube is taking too long to start ==="</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Wait 5-10 minutes for first startup"</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Monitor logs: sudo tail -f /opt/sonarqube/logs/sonar.log"</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 2: If there are memory issues ==="</span>
free -h
sudo nano /opt/sonarqube/conf/sonar.properties
cat &lt;&lt; <span class="hljs-string">'EOF'</span>
<span class="hljs-comment"># Reduce memory usage</span>
sonar.web.javaOpts=-Xms512m -Xmx1g
sonar.ce.javaOpts=-Xms512m -Xmx1g
sonar.search.javaOpts=-Xms512m -Xmx1g
EOF

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 3: If Elasticsearch won't start ==="</span>
sysctl vm.max_map_count
sudo sysctl -w vm.max_map_count=262144
<span class="hljs-built_in">echo</span> <span class="hljs-string">'vm.max_map_count=262144'</span> | sudo tee -a /etc/sysctl.conf

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 4: If there are permission issues ==="</span>
sudo chown -R sonarqube:sonarqube /opt/sonarqube
id sonarqube

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 5: If port 9000 is in use ==="</span>
sudo lsof -i :9000
sudo nano /opt/sonarqube/conf/sonar.properties
<span class="hljs-comment"># Change sonar.web.port=9001 if needed</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 6: Restart SonarQube service ==="</span>
sudo systemctl restart sonarqube
sudo systemctl status sonarqube

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Fix 7: Manual start if still not working ==="</span>
sudo systemctl stop sonarqube
sudo -u sonarqube /opt/sonarqube/bin/linux-x86-64/sonar.sh start
sudo -u sonarqube /opt/sonarqube/bin/linux-x86-64/sonar.sh console
</code></pre>
<h3 id="heading-4-install-sonarqube-scanner">4. Install SonarQube Scanner</h3>
<p>The scanner analyzes your code and sends results to the SonarQube server.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># ===========================================</span>
<span class="hljs-comment"># Install SonarQube Scanner on Linux</span>
<span class="hljs-comment"># ===========================================</span>

<span class="hljs-comment"># 1. Download SonarQube Scanner</span>
<span class="hljs-built_in">cd</span> /opt
sudo wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip

<span class="hljs-comment"># 2. Extract Scanner</span>
sudo unzip sonar-scanner-cli-5.0.1.3006-linux.zip
sudo mv sonar-scanner-5.0.1.3006-linux sonar-scanner
sudo chown -R $(whoami):$(whoami) /opt/sonar-scanner

<span class="hljs-comment"># 3. Add Scanner to PATH</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">'export PATH=$PATH:/opt/sonar-scanner/bin'</span> &gt;&gt; ~/.bashrc
<span class="hljs-built_in">source</span> ~/.bashrc

<span class="hljs-comment"># 4. Verify installation</span>
sonar-scanner --version

<span class="hljs-comment"># 5. Configure Scanner (optional)</span>
sudo nano /opt/sonar-scanner/conf/sonar-scanner.properties

<span class="hljs-comment"># Enter the URL in the browser and setup the SonarQube using dashboard</span>
http://localhost:9000

<span class="hljs-comment"># Username: admin</span>
<span class="hljs-comment"># Password: admin</span>
</code></pre>
<h3 id="heading-logging-into-sonarqube-web-dashboard">🔐 Logging into SonarQube Web Dashboard</h3>
<ol>
<li><p>Open your browser and go to:<br /> <a target="_blank" href="http://localhost:9000"><code>http://localhost:9000</code></a></p>
</li>
<li><p>You will be prompted for login credentials.</p>
<ul>
<li><p><strong>Default username:</strong> <code>admin</code></p>
</li>
<li><p><strong>Default password:</strong> <code>admin</code></p>
</li>
</ul>
</li>
<li><p><strong>Important:</strong> After first login, immediately change the password to something secure.</p>
</li>
<li><p>Next, you will be prompted to <strong>create a new project</strong>:</p>
<ul>
<li><p>Enter a <strong>project name</strong> (e.g., <code>Email Subject Generator</code>)</p>
</li>
<li><p>Enter a <strong>project key</strong> (a unique identifier, e.g., <code>email-subject-gen</code>)</p>
</li>
</ul>
</li>
<li><p>Choose whether the project will be connected as a <strong>local</strong> or <strong>remote</strong> project depending on your setup.</p>
</li>
<li><p>After configuration, you will be taken to the <strong>SonarQube project dashboard</strong>, where you can view metrics, code quality reports, and other insights.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752313660153/cc1b17a4-bc7c-4353-82b8-c33efcd1733e.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-configure-system"><mark>Configure System</mark></h3>
<p>To integrate SonarQube scanning within your Jenkins pipeline, follow these steps to properly add SonarQube server details in Jenkins:</p>
<ol>
<li><p>Open Jenkins dashboard and navigate to:<br /> <strong>Manage Jenkins</strong> → <strong>Configure System</strong></p>
</li>
<li><p>Scroll down to the <strong>SonarQube servers</strong> section.</p>
</li>
<li><p>Click <strong>Add SonarQube</strong>.</p>
</li>
<li><p>Enter the following details:</p>
<ul>
<li><p><strong>Name:</strong> <code>SonarQube</code> (or any recognizable name)</p>
</li>
<li><p><strong>Server URL:</strong> <code>http://&lt;your-sonarqube-server-ip&gt;:9000</code> (e.g., <code>http://localhost:9000</code>)</p>
</li>
<li><p><strong>Server authentication token:</strong></p>
<ul>
<li><p>Generate a token from SonarQube user profile → <strong>My Account</strong> → <strong>Security</strong> → <strong>Generate Tokens</strong></p>
</li>
<li><p>Paste the generated token here to authenticate Jenkins with SonarQube.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Save the configuration.</p>
</li>
<li><p>Now, Jenkins can communicate with SonarQube to run code analysis as part of your CI pipeline.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752313818845/ac8677d2-2f37-4dca-bf83-0e835b1e887f.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-manage-credentials"><mark>Manage Credentials</mark></h3>
<p>Jenkins needs credentials to securely access services like Bitbucket, Docker Hub, and SonarQube. You add these credentials in Jenkins under <strong>Manage Jenkins &gt; Manage Credentials</strong>.</p>
<ul>
<li><p><strong>Bitbucket:</strong> Use your username and app password or token to allow Jenkins to clone and push code.</p>
</li>
<li><p><strong>Docker Hub:</strong> Provide your Docker username and access token so Jenkins can build and push images.</p>
</li>
<li><p><strong>SonarQube:</strong> Use a secret token from your SonarQube account for Jenkins to run code analysis.</p>
</li>
</ul>
<p>Always store credentials securely in Jenkins and avoid hardcoding them in your code or pipeline scripts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752314090464/507a1c21-964e-401c-9af4-4e1cdbff385f.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-step-6-setup-ci-pipeline">✅ <strong>Step 6: Setup CI Pipeline</strong></h2>
<p><strong>Prerequisites Checklist</strong></p>
<ul>
<li><p>Jenkins is installed and running</p>
</li>
<li><p>Bitbucket repository is accessible and configured with Jenkins credentials</p>
</li>
<li><p>Docker is installed and Jenkins user added to docker group</p>
</li>
<li><p>SonarQube server is running and integrated with Jenkins (SonarQube plugin configured)</p>
</li>
<li><p>Python and required build tools installed on Jenkins node (optional if you use Docker build)</p>
</li>
</ul>
<h4 id="heading-script-for-ci-pipeline">Script for CI pipeline</h4>
<pre><code class="lang-bash">pipeline {
    agent any

    // Add trigger configuration
    triggers {
        // Poll SCM every 2 minutes as fallback
        pollSCM(<span class="hljs-string">'H/2 * * * *'</span>)
        // Bitbucket webhook trigger
        bitbucketPush()
    }

    environment {
        VENV = <span class="hljs-string">'venv'</span>
        DOCKER_HUB_USER = <span class="hljs-string">'YOUR-DOCKER-HUB-USERNAME'</span>
        IMAGE_TAG = <span class="hljs-string">"<span class="hljs-variable">${BUILD_NUMBER}</span>"</span>
        FRONTEND_IMAGE = <span class="hljs-string">"<span class="hljs-variable">${DOCKER_HUB_USER}</span>/email-subject-generator-frontend"</span>
        BACKEND_IMAGE = <span class="hljs-string">"<span class="hljs-variable">${DOCKER_HUB_USER}</span>/email-subject-generator-backend"</span>
        DOCKER_HUB_CREDENTIALS = <span class="hljs-string">'docker-hub-credentials'</span>
        // SonarQube configuration
        SONAR_HOST_URL = <span class="hljs-string">'http://localhost:9000'</span>
        SONAR_PROJECT_KEY = <span class="hljs-string">'email-subject-generator'</span>
        SONAR_PROJECT_NAME = <span class="hljs-string">'Email Subject Generator'</span>
    }

    stages {
        stage(<span class="hljs-string">'Clone Repository'</span>) {
            steps {
                script {
                    // Clone the repository
                    checkout([
                        <span class="hljs-variable">$class</span>: <span class="hljs-string">'GitSCM'</span>,
                        branches: [[name: <span class="hljs-string">'*/main'</span>]],
                        userRemoteConfigs: [[
                            url: <span class="hljs-string">'https://sachindumalshan@bitbucket.org/sachindu-work-space/email-subject-generator.git'</span>,
                            credentialsId: <span class="hljs-string">'bitbucket-creds'</span>
                        ]],
                        extensions: [
                            [<span class="hljs-variable">$class</span>: <span class="hljs-string">'CleanBeforeCheckout'</span>],
                            [<span class="hljs-variable">$class</span>: <span class="hljs-string">'PruneStaleBranch'</span>]
                        ]
                    ])

                    // Set git commit <span class="hljs-built_in">hash</span> after cloning
                    env.GIT_COMMIT_SHORT = sh(script: <span class="hljs-string">"git rev-parse --short HEAD"</span>, returnStdout: <span class="hljs-literal">true</span>).trim()
                    env.GIT_COMMIT_MESSAGE = sh(script: <span class="hljs-string">"git log -1 --pretty=%B"</span>, returnStdout: <span class="hljs-literal">true</span>).trim()
                    env.GIT_AUTHOR = sh(script: <span class="hljs-string">"git log -1 --pretty=%an"</span>, returnStdout: <span class="hljs-literal">true</span>).trim()

                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Git commit: <span class="hljs-variable">${env.GIT_COMMIT_SHORT}</span>"</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Commit message: <span class="hljs-variable">${env.GIT_COMMIT_MESSAGE}</span>"</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Author: <span class="hljs-variable">${env.GIT_AUTHOR}</span>"</span>
                }
            }
        }

        stage(<span class="hljs-string">'Set up Python Environment'</span>) {
            steps {
                sh <span class="hljs-string">''</span><span class="hljs-string">'
                    # Clean up any existing venv
                    rm -rf "$VENV"

                    # Install python3-venv if not available
                    if ! python3 -m venv --help &gt; /dev/null 2&gt;&amp;1; then
                        echo "Installing python3-venv package..."
                        sudo apt update
                        sudo apt install -y python3-venv python3-pip
                    fi

                    # Create fresh virtual environment
                    python3 -m venv "$VENV"
                    . "$VENV/bin/activate"

                    # Upgrade pip
                    pip install --upgrade pip

                    # Install dependencies with error handling
                    if [ -f backend/requirements.txt ]; then
                        echo "Installing backend dependencies..."
                        pip install -r backend/requirements.txt
                    else
                        echo "backend/requirements.txt not found"
                        exit 1
                    fi

                    # Check if frontend has requirements.txt (optional)
                    if [ -f frontend/requirements.txt ]; then
                        echo "Installing frontend dependencies..."
                        pip install -r frontend/requirements.txt
                    else
                        echo "frontend/requirements.txt not found, skipping frontend Python dependencies"
                    fi

                    # Install coverage for Python code coverage
                    pip install coverage pytest pytest-cov

                    # Verify installation
                    pip list
                '</span><span class="hljs-string">''</span>
            }
        }

        stage(<span class="hljs-string">'Run Tests with Coverage'</span>) {
            steps {
                script {
                    try {
                        sh <span class="hljs-string">''</span><span class="hljs-string">'
                            . "$VENV/bin/activate"

                            # Set timeout for tests
                            timeout 300 bash -c '</span>
                                <span class="hljs-comment"># Create coverage reports directory</span>
                                mkdir -p coverage-reports

                                <span class="hljs-comment"># Run frontend tests with coverage</span>
                                <span class="hljs-keyword">if</span> [ -f frontend/app.py ]; <span class="hljs-keyword">then</span>
                                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Running frontend tests with coverage..."</span>
                                    <span class="hljs-built_in">cd</span> frontend
                                    coverage run --<span class="hljs-built_in">source</span>=. -m pytest --junitxml=../coverage-reports/frontend-junit.xml . || python app.py --<span class="hljs-built_in">test</span>
                                    coverage xml -o ../coverage-reports/frontend-coverage.xml
                                    coverage report
                                    <span class="hljs-built_in">cd</span> ..
                                <span class="hljs-keyword">else</span>
                                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Frontend app.py not found, skipping frontend tests"</span>
                                <span class="hljs-keyword">fi</span>

                                <span class="hljs-comment"># Run backend tests with coverage</span>
                                <span class="hljs-keyword">if</span> [ -f backend/app.py ]; <span class="hljs-keyword">then</span>
                                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Running backend tests with coverage..."</span>
                                    <span class="hljs-built_in">cd</span> backend
                                    coverage run --<span class="hljs-built_in">source</span>=. -m pytest --junitxml=../coverage-reports/backend-junit.xml . || python app.py --<span class="hljs-built_in">test</span>
                                    coverage xml -o ../coverage-reports/backend-coverage.xml
                                    coverage report
                                    <span class="hljs-built_in">cd</span> ..
                                <span class="hljs-keyword">else</span>
                                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Backend app.py not found, skipping backend tests"</span>
                                <span class="hljs-keyword">fi</span>
                            <span class="hljs-string">'
                        '</span><span class="hljs-string">''</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Tests failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        currentBuild.result = <span class="hljs-string">'UNSTABLE'</span>
                    }
                }
            }
        }

        stage(<span class="hljs-string">'SonarQube Analysis'</span>) {
            steps {
                script {
                    try {
                        <span class="hljs-keyword">if</span> (!fileExists(<span class="hljs-string">'sonar-project.properties'</span>)) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"sonar-project.properties file not found. Creating default configuration..."</span>

                        // Create sonar-project.properties file
                        writeFile file: <span class="hljs-string">'sonar-project.properties'</span>, text: <span class="hljs-string">""</span><span class="hljs-string">"sonar.projectKey=<span class="hljs-variable">${env.SONAR_PROJECT_KEY}</span>
sonar.projectName=<span class="hljs-variable">${env.SONAR_PROJECT_NAME}</span>
sonar.projectVersion=<span class="hljs-variable">${env.BUILD_NUMBER}</span>
sonar.sources=frontend,backend
sonar.sourceEncoding=UTF-8
sonar.python.coverage.reportPaths=coverage-reports/frontend-coverage.xml,coverage-reports/backend-coverage.xml
sonar.python.xunit.reportPath=coverage-reports/frontend-junit.xml,coverage-reports/backend-junit.xml
sonar.exclusions=**/*.pyc,**/__pycache__/**,**/venv/**,**/node_modules/**,**/*.log,**/database/**,**/static/**,**/templates/**
sonar.tests=frontend,backend
sonar.test.inclusions=**/test_*.py,**/*test*.py
sonar.scm.provider=git
sonar.scm.revision=<span class="hljs-variable">${env.GIT_COMMIT_SHORT}</span>"</span><span class="hljs-string">""</span>
                        }<span class="hljs-keyword">else</span>{
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"sonar-project.properties file found. Using existing configuration..."</span>
                        }

                        // Run SonarQube analysis
                        withSonarQubeEnv(<span class="hljs-string">'SonarQube'</span>) {
                            sh <span class="hljs-string">''</span><span class="hljs-string">'
                                echo "Running SonarQube analysis..."
                                echo "Project Key: $SONAR_PROJECT_KEY"
                                echo "SonarQube Host: $SONAR_HOST_URL"

                                # Use sonar-scanner with project file
                                sonar-scanner
                            '</span><span class="hljs-string">''</span>
                        }

                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"SonarQube analysis failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        currentBuild.result = <span class="hljs-string">'UNSTABLE'</span>
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Lightweight Code Quality'</span>) {
            steps {
                script {
                    try {
                        sh <span class="hljs-string">''</span><span class="hljs-string">'
                            . "$VENV/bin/activate"

                            echo "Running lightweight code quality checks..."

                            # Quick Python syntax check
                            echo "Checking Python syntax..."
                            python -m py_compile backend/app.py || echo "Backend syntax issues found"
                            python -m py_compile frontend/app.py || echo "Frontend syntax issues found"

                            # Quick linting with basic rules
                            echo "Running basic linting..."
                            pip install flake8 || true
                            flake8 --select=E9,F63,F7,F82 backend/ frontend/ || echo "Basic linting issues found"

                            # Check for common security issues
                            echo "Basic security check..."
                            grep -r "password.*=" backend/ frontend/ || echo "No hardcoded passwords found"

                            echo "Lightweight quality checks completed!"
                        '</span><span class="hljs-string">''</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Lightweight quality checks failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Continuing anyway..."</span>
                    }

                    // Always <span class="hljs-built_in">continue</span> - never fail the pipeline
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"SonarQube analysis running in background at: <span class="hljs-variable">${env.SONAR_HOST_URL}</span>/dashboard?id=<span class="hljs-variable">${env.SONAR_PROJECT_KEY}</span>"</span>
                }
            }
        }

        stage(<span class="hljs-string">'Build Docker Images'</span>) {
            steps {
                script {
                    // Build frontend image
                    <span class="hljs-keyword">if</span> (fileExists(<span class="hljs-string">'frontend/Dockerfile'</span>)) {
                        sh <span class="hljs-string">''</span><span class="hljs-string">'
                            echo "Building frontend Docker image..."
                            echo "Dockerfile content:"
                            cat frontend/Dockerfile
                            echo "---"

                            # Force rebuild without cache to ensure fresh build
                            docker build --no-cache -t "$FRONTEND_IMAGE:$IMAGE_TAG" -t "$FRONTEND_IMAGE:latest" ./frontend
                        '</span><span class="hljs-string">''</span>
                    } <span class="hljs-keyword">else</span> {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Frontend Dockerfile not found, skipping frontend build"</span>
                    }

                    // Build backend image
                    <span class="hljs-keyword">if</span> (fileExists(<span class="hljs-string">'backend/Dockerfile'</span>)) {
                        sh <span class="hljs-string">''</span><span class="hljs-string">'
                            echo "Building backend Docker image..."
                            echo "Dockerfile content:"
                            cat backend/Dockerfile
                            echo "---"

                            # Force rebuild without cache to ensure fresh build
                            docker build --no-cache -t "$BACKEND_IMAGE:$IMAGE_TAG" -t "$BACKEND_IMAGE:latest" ./backend
                        '</span><span class="hljs-string">''</span>
                    } <span class="hljs-keyword">else</span> {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Backend Dockerfile not found, skipping backend build"</span>
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Test Docker Images'</span>) {
            steps {
                script {
                    try {
                        // Test frontend image
                        <span class="hljs-keyword">if</span> (sh(script: <span class="hljs-string">"docker images -q \"\$FRONTEND_IMAGE:\$IMAGE_TAG\""</span>, returnStdout: <span class="hljs-literal">true</span>).trim()) {
                            sh <span class="hljs-string">''</span><span class="hljs-string">'
                                echo "Testing frontend Docker image..."

                                # Clean up any existing test containers
                                docker stop frontend-test 2&gt;/dev/null || true
                                docker rm frontend-test 2&gt;/dev/null || true

                                # Find an available port for frontend
                                PORT1=5000
                                while netstat -tulpn | grep -q ":$PORT1 "; do
                                    PORT1=$((PORT1 + 2))
                                done
                                echo "Using port $PORT1 for frontend testing..."

                                # Run frontend container in background
                                docker run -d --name frontend-test -p $PORT1:5000 "$FRONTEND_IMAGE:$IMAGE_TAG"

                                # Wait for container to start
                                echo "Waiting for frontend container to start..."
                                sleep 15

                                # Check if container is running
                                if docker ps | grep -q frontend-test; then
                                    echo "Frontend container is running successfully on port $PORT1"

                                    # Test if the application is responding
                                    echo "Testing frontend health..."
                                    timeout 30 bash -c "until curl -f http://localhost:$PORT1 &gt;/dev/null 2&gt;&amp;1; do sleep 2; done" || echo "Frontend health check timeout - this might be normal if no health endpoint exists"

                                    # Check container logs for any obvious errors
                                    echo "Frontend container logs:"
                                    docker logs frontend-test --tail 20
                                else
                                    echo "Frontend container failed to start"
                                    docker logs frontend-test
                                    exit 1
                                fi

                                # Clean up
                                docker stop frontend-test
                                docker rm frontend-test
                            '</span><span class="hljs-string">''</span>
                        } <span class="hljs-keyword">else</span> {
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Frontend image not found, skipping frontend test"</span>
                        }

                        // Test backend image
                        <span class="hljs-keyword">if</span> (sh(script: <span class="hljs-string">"docker images -q \"\$BACKEND_IMAGE:\$IMAGE_TAG\""</span>, returnStdout: <span class="hljs-literal">true</span>).trim()) {
                            sh <span class="hljs-string">''</span><span class="hljs-string">'
                                echo "Testing backend Docker image..."

                                # Clean up any existing test containers
                                docker stop backend-test 2&gt;/dev/null || true
                                docker rm backend-test 2&gt;/dev/null || true

                                # Find an available port for backend
                                PORT2=5001
                                while netstat -tulpn | grep -q ":$PORT2 "; do
                                    PORT2=$((PORT2 + 2))
                                done
                                echo "Using port $PORT2 for backend testing..."

                                # Run backend container in background
                                docker run -d --name backend-test -p $PORT2:5001 "$BACKEND_IMAGE:$IMAGE_TAG"

                                # Wait for container to start
                                echo "Waiting for backend container to start..."
                                sleep 15

                                # Check if container is running
                                if docker ps | grep -q backend-test; then
                                    echo "Backend container is running successfully on port $PORT2"

                                    # Test if the application is responding
                                    echo "Testing backend health..."
                                    timeout 30 bash -c "until curl -f http://localhost:$PORT2 &gt;/dev/null 2&gt;&amp;1; do sleep 2; done" || echo "Backend health check timeout - this might be normal if no health endpoint exists"

                                    # Check container logs for any obvious errors
                                    echo "Backend container logs:"
                                    docker logs backend-test --tail 20
                                else
                                    echo "Backend container failed to start"
                                    docker logs backend-test
                                    exit 1
                                fi

                                # Clean up
                                docker stop backend-test
                                docker rm backend-test
                            '</span><span class="hljs-string">''</span>
                        } <span class="hljs-keyword">else</span> {
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Backend image not found, skipping backend test"</span>
                        }

                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Image tests failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        currentBuild.result = <span class="hljs-string">'UNSTABLE'</span>

                        // Clean up on failure
                        sh <span class="hljs-string">''</span><span class="hljs-string">'
                            docker stop frontend-test backend-test 2&gt;/dev/null || true
                            docker rm frontend-test backend-test 2&gt;/dev/null || true
                        '</span><span class="hljs-string">''</span>
                    }
                }
    }
}

        stage(<span class="hljs-string">'Push Docker Images'</span>) {
            when {
                // Only push <span class="hljs-keyword">if</span> quality gate passed and build is successful
                anyOf {
                    expression { currentBuild.result == null }
                    expression { currentBuild.result == <span class="hljs-string">'SUCCESS'</span> }
                }
            }
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Pushing Docker images to Docker Hub..."</span>
                        withDockerRegistry([credentialsId: <span class="hljs-string">'docker-hub-credentials'</span>, url: <span class="hljs-string">'https://index.docker.io/v1/'</span>]) {
                            sh <span class="hljs-string">''</span><span class="hljs-string">'
                                # Push frontend images
                                echo "Pushing frontend images..."
                                docker push "$FRONTEND_IMAGE:$IMAGE_TAG"
                                docker push "$FRONTEND_IMAGE:latest"

                                # Push backend images
                                echo "Pushing backend images..."
                                docker push "$BACKEND_IMAGE:$IMAGE_TAG"
                                docker push "$BACKEND_IMAGE:latest"
                            '</span><span class="hljs-string">''</span>
                        }
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Successfully pushed all Docker images"</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to push Docker images: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        throw e
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Trigger CD Pipeline'</span>) {
            when {
                anyOf {
                        expression { currentBuild.result == null }
                        expression { currentBuild.result == <span class="hljs-string">'SUCCESS'</span> }
                    }
            }
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Triggering CD pipeline for automatic deployment..."</span>

                        // Get the current build number to use as image tag
                        def imageTag = env.BUILD_NUMBER

                        // Trigger CD pipeline <span class="hljs-keyword">for</span> dev environment automatically
                        build job: <span class="hljs-string">'email-gen-app-cd'</span>, 
                              parameters: [
                                  string(name: <span class="hljs-string">'IMAGE_TAG'</span>, value: imageTag),
                                  booleanParam(name: <span class="hljs-string">'SKIP_TESTS'</span>, value: <span class="hljs-literal">false</span>)
                              ],
                              <span class="hljs-built_in">wait</span>: <span class="hljs-literal">false</span>  // Don<span class="hljs-string">'t wait for CD to complete

                        echo "CD pipeline triggered successfully"
                        echo "Image tag: ${imageTag}"
                        echo "Frontend image: ${env.FRONTEND_IMAGE}:${imageTag}"
                        echo "Backend image: ${env.BACKEND_IMAGE}:${imageTag}"
                        echo "CD pipeline will deploy to ports - Frontend: 5000, Backend: 5001, DB: 3306"

                    } catch (Exception e) {
                        echo "Failed to trigger CD pipeline: ${e.getMessage()}"
                        echo "You can manually trigger the CD pipeline with the following parameters:"
                        echo "- IMAGE_TAG: ${env.BUILD_NUMBER}"
                        echo "- SKIP_TESTS: false"
                        echo "Images available for deployment:"
                        echo "- Frontend: ${env.FRONTEND_IMAGE}:${env.BUILD_NUMBER}"
                        echo "- Backend: ${env.BACKEND_IMAGE}:${env.BUILD_NUMBER}"
                        // Don'</span>t fail the CI pipeline <span class="hljs-keyword">if</span> CD trigger fails
                    }
                }
            }
        }
    }

    post {
        always {
            script {
                // Ensure we<span class="hljs-string">'re in a node context for cleanup
                try {
                    // Clean up virtual environment
                    sh '</span><span class="hljs-string">''</span>
                        <span class="hljs-keyword">if</span> [ -d <span class="hljs-string">"<span class="hljs-variable">$VENV</span>"</span> ]; <span class="hljs-keyword">then</span>
                            rm -rf <span class="hljs-string">"<span class="hljs-variable">$VENV</span>"</span>
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Cleaned up virtual environment"</span>
                        <span class="hljs-keyword">fi</span>
                    <span class="hljs-string">''</span><span class="hljs-string">'

                    // Clean up SonarQube working directory
                    sh '</span><span class="hljs-string">''</span>
                        <span class="hljs-keyword">if</span> [ -d <span class="hljs-string">".sonar"</span> ]; <span class="hljs-keyword">then</span>
                            rm -rf .sonar
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Cleaned up SonarQube working directory"</span>
                        <span class="hljs-keyword">fi</span>
                    <span class="hljs-string">''</span><span class="hljs-string">'

                    // Clean up coverage reports
                    sh '</span><span class="hljs-string">''</span>
                        <span class="hljs-keyword">if</span> [ -d <span class="hljs-string">"coverage-reports"</span> ]; <span class="hljs-keyword">then</span>
                            rm -rf coverage-reports
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Cleaned up coverage reports"</span>
                        <span class="hljs-keyword">fi</span>
                    <span class="hljs-string">''</span><span class="hljs-string">'

                    // Clean up Docker images to save space
                    sh '</span><span class="hljs-string">''</span>
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Cleaning up Docker images..."</span>
                        docker image prune -f

                        <span class="hljs-comment"># Clean up any remaining test containers</span>
                        docker stop frontend-test backend-test 2&gt;/dev/null || <span class="hljs-literal">true</span>
                        docker rm frontend-test backend-test 2&gt;/dev/null || <span class="hljs-literal">true</span>
                    <span class="hljs-string">''</span><span class="hljs-string">'
                } catch (Exception e) {
                    echo "Cleanup failed: ${e.getMessage()}"
                }
            }
        }
        success {
            echo '</span>Pipeline completed successfully!<span class="hljs-string">'
            echo "Frontend image: ${env.FRONTEND_IMAGE}:${env.IMAGE_TAG}"
            echo "Backend image: ${env.BACKEND_IMAGE}:${env.IMAGE_TAG}"
            echo "SonarQube analysis completed. Check ${env.SONAR_HOST_URL}/dashboard?id=${env.SONAR_PROJECT_KEY}"
            echo "Triggered by commit: ${env.GIT_COMMIT_SHORT} by ${env.GIT_AUTHOR}"
            echo "Commit message: ${env.GIT_COMMIT_MESSAGE}"
        }
        failure {
            echo '</span>Pipeline failed!<span class="hljs-string">'
            echo "Failed commit: ${env.GIT_COMMIT_SHORT} by ${env.GIT_AUTHOR}"
            echo "Commit message: ${env.GIT_COMMIT_MESSAGE}"
        }
        unstable {
            echo '</span>Pipeline completed but tests or quality gate failed!<span class="hljs-string">'
            echo "Unstable commit: ${env.GIT_COMMIT_SHORT} by ${env.GIT_AUTHOR}"
        }
    }
}</span>
</code></pre>
<p><strong>Instructions to Create and Test the Pipeline</strong></p>
<ol>
<li><p>Login to Jenkins dashboard.</p>
</li>
<li><p>Click <strong>“New Item”</strong>.</p>
<ul>
<li><p>Enter name: <code>email-gen-ci</code></p>
</li>
<li><p>Select <strong>Pipeline</strong>, click <strong>OK</strong>.</p>
</li>
</ul>
</li>
<li><p>Scroll to <strong>Pipeline section</strong> at bottom.</p>
</li>
<li><p>Paste the <strong>above Groovy script</strong>.</p>
</li>
<li><p>Configure the following <strong>credentials and environment values</strong> in Jenkins:</p>
<ul>
<li><p><code>bitbucket-credentials-id</code> – your Bitbucket username/password or token ID</p>
</li>
<li><p><code>dockerhub-credentials-id</code> – your Docker Hub username/password credentials ID</p>
</li>
<li><p>Replace <code>your-dockerhub-username</code> with your actual Docker Hub username</p>
</li>
<li><p>Replace Bitbucket repo URL with your repository URL</p>
</li>
<li><p><code>SonarQubeServer</code> – configured name under <strong>Manage Jenkins &gt; Configure System &gt; SonarQube servers</strong></p>
</li>
</ul>
</li>
<li><p>Click <strong>“Save”</strong>.</p>
</li>
<li><p>Click <strong>“Build Now”</strong> to test the pipeline.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752332306916/9e3b0d8c-9d35-41da-bb0c-2e6ad3337a90.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-verify-the-ci-pipeline">Verify the CI Pipeline</h3>
<ul>
<li><p>Build starts and shows each stage in Blue Ocean or classic view</p>
</li>
<li><p><strong>Checkout</strong> pulls your Bitbucket code</p>
</li>
<li><p><strong>Code Quality Analysis</strong> stage executes SonarQube scan and updates Sonar dashboard</p>
</li>
<li><p><strong>Build</strong> stage builds Docker image</p>
</li>
<li><p><strong>Test</strong> stage runs container and executes pytest tests inside</p>
</li>
<li><p><strong>Push Docker Image</strong> stage pushes image to Docker Hub</p>
</li>
</ul>
<hr />
<h2 id="heading-step-7-setup-cd-pipeline">✅ <strong>Step 7: Setup CD Pipeline</strong></h2>
<pre><code class="lang-bash">pipeline {
    agent any

    parameters {
        string(
            name: <span class="hljs-string">'IMAGE_TAG'</span>,
            defaultValue: <span class="hljs-string">'latest'</span>,
            description: <span class="hljs-string">'Docker image tag to deploy (e.g., latest, 123, v1.0.0)'</span>
        )
        booleanParam(
            name: <span class="hljs-string">'SKIP_TESTS'</span>,
            defaultValue: <span class="hljs-literal">false</span>,
            description: <span class="hljs-string">'Skip deployment tests'</span>
        )
    }

    environment {
        DOCKER_HUB_USER = <span class="hljs-string">'YOUR-DOCKER-HUB-USERNAME'</span>
        FRONTEND_IMAGE = <span class="hljs-string">"<span class="hljs-variable">${DOCKER_HUB_USER}</span>/email-subject-generator-frontend"</span>
        BACKEND_IMAGE = <span class="hljs-string">"<span class="hljs-variable">${DOCKER_HUB_USER}</span>/email-subject-generator-backend"</span>
        DOCKER_HUB_CREDENTIALS = <span class="hljs-string">'docker-hub-credentials'</span>

        // Fixed ports <span class="hljs-keyword">for</span> single deployment
        FRONTEND_PORT = <span class="hljs-string">'5000'</span>
        BACKEND_PORT = <span class="hljs-string">'5001'</span>
        DB_PORT = <span class="hljs-string">'3306'</span>
    }

    stages {
        stage(<span class="hljs-string">'Validate Parameters'</span>) {
            steps {
                script {
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Deployment Configuration ==="</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Image Tag: <span class="hljs-variable">${params.IMAGE_TAG}</span>"</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Skip Tests: <span class="hljs-variable">${params.SKIP_TESTS}</span>"</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Frontend Image: <span class="hljs-variable">${env.FRONTEND_IMAGE}</span>:<span class="hljs-variable">${params.IMAGE_TAG}</span>"</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Backend Image: <span class="hljs-variable">${env.BACKEND_IMAGE}</span>:<span class="hljs-variable">${params.IMAGE_TAG}</span>"</span>
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Ports - Frontend: <span class="hljs-variable">${env.FRONTEND_PORT}</span>, Backend: <span class="hljs-variable">${env.BACKEND_PORT}</span>, DB: <span class="hljs-variable">${env.DB_PORT}</span>"</span>
                }
            }
        }

        stage(<span class="hljs-string">'Clone Repository'</span>) {
            steps {
                script {
                    checkout([
                        <span class="hljs-variable">$class</span>: <span class="hljs-string">'GitSCM'</span>,
                        branches: [[name: <span class="hljs-string">'*/main'</span>]],
                        userRemoteConfigs: [[
                            url: <span class="hljs-string">'https://sachindumalshan@bitbucket.org/sachindu-work-space/email-subject-generator.git'</span>,
                            credentialsId: <span class="hljs-string">'bitbucket-creds'</span>
                        ]],
                        extensions: [
                            [<span class="hljs-variable">$class</span>: <span class="hljs-string">'CleanBeforeCheckout'</span>]
                        ]
                    ])

                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Repository cloned successfully"</span>
                }
            }
        }

        stage(<span class="hljs-string">'Prepare Environment'</span>) {
            steps {
                script {
                    // Check <span class="hljs-keyword">if</span> docker-compose.yml file exists
                    <span class="hljs-keyword">if</span> (!fileExists(<span class="hljs-string">'docker-compose.yml'</span>)) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"docker-compose.yml file not found. Creating default configuration..."</span>

                        // Create deployment-ready docker-compose file
                        def composeContent = <span class="hljs-string">""</span><span class="hljs-string">"version: '3.9'

        services:
          frontend:
            image: <span class="hljs-variable">${env.FRONTEND_IMAGE}</span>:<span class="hljs-variable">${params.IMAGE_TAG}</span>
            container_name: emailgen-frontend
            ports:
              - "</span><span class="hljs-variable">${env.FRONTEND_PORT}</span>:5000<span class="hljs-string">"
            depends_on:
              - backend
            environment:
              - BACKEND_URL=http://backend:5001
            networks:
              - emailgen-net
            restart: unless-stopped

          backend:
            image: <span class="hljs-variable">${env.BACKEND_IMAGE}</span>:<span class="hljs-variable">${params.IMAGE_TAG}</span>
            container_name: emailgen-backend
            ports:
              - "</span><span class="hljs-variable">${env.BACKEND_PORT}</span>:5001<span class="hljs-string">"
            depends_on:
              - db
            environment:
              - MYSQL_HOST=db
              - MYSQL_PORT=3306
              - MYSQL_DATABASE=email_generator
              - MYSQL_USER=root
              - MYSQL_PASSWORD=rootpassword123
              - GROQ_API_KEY=YOUR-GROQ-API-KEY
            networks:
              - emailgen-net
            restart: unless-stopped

          db:
            image: mysql:8.0
            container_name: emailgen-db
            restart: unless-stopped
            environment:
              - MYSQL_ROOT_PASSWORD=rootpassword123
              - MYSQL_DATABASE=email_generator
            volumes:
              - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
              - mysql_data:/var/lib/mysql
            ports:
              - "</span><span class="hljs-variable">${env.DB_PORT}</span>:3306<span class="hljs-string">"
            networks:
              - emailgen-net
            healthcheck:
              test: ["</span>CMD<span class="hljs-string">", "</span>mysqladmin<span class="hljs-string">", "</span>ping<span class="hljs-string">", "</span>-h<span class="hljs-string">", "</span>localhost<span class="hljs-string">", "</span>-p=rootpassword123<span class="hljs-string">"]
              interval: 10s
              timeout: 5s
              retries: 5

        volumes:
          mysql_data:

        networks:
          emailgen-net:
            driver: bridge
        "</span><span class="hljs-string">""</span>

                        // Write the compose file
                        writeFile file: <span class="hljs-string">"docker-compose.yml"</span>, text: composeContent
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Docker-compose file created with deployment configuration"</span>

                    } <span class="hljs-keyword">else</span> {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"docker-compose.yml file found. Using existing configuration..."</span>

                        // Check <span class="hljs-keyword">if</span> existing compose file uses build or image
                        def composeContent = readFile(<span class="hljs-string">'docker-compose.yml'</span>)

                        <span class="hljs-keyword">if</span> (composeContent.contains(<span class="hljs-string">'build:'</span>) &amp;&amp; !composeContent.contains(<span class="hljs-string">'image:'</span>)) {
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Existing compose file uses 'build' configuration."</span>
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Setting environment variables for potential image override..."</span>

                            // Set environment variables that can be used <span class="hljs-keyword">if</span> needed
                            env.FRONTEND_IMAGE = env.FRONTEND_IMAGE ?: <span class="hljs-string">'emailgen-frontend'</span>
                            env.BACKEND_IMAGE = env.BACKEND_IMAGE ?: <span class="hljs-string">'emailgen-backend'</span>
                            env.IMAGE_TAG = params.IMAGE_TAG ?: <span class="hljs-string">'latest'</span>

                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Environment variables set:"</span>
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"FRONTEND_IMAGE: <span class="hljs-variable">${env.FRONTEND_IMAGE}</span>"</span>
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"BACKEND_IMAGE: <span class="hljs-variable">${env.BACKEND_IMAGE}</span>"</span>
                            <span class="hljs-built_in">echo</span> <span class="hljs-string">"IMAGE_TAG: <span class="hljs-variable">${env.IMAGE_TAG}</span>"</span>
                        }

                        // Display existing compose file content <span class="hljs-keyword">for</span> verification
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Current docker-compose.yml content:"</span>
                        sh <span class="hljs-string">'cat docker-compose.yml'</span>
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Pull Docker Images'</span>) {
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Pulling Docker images..."</span>
                        withDockerRegistry([credentialsId: <span class="hljs-string">'docker-hub-credentials'</span>, url: <span class="hljs-string">'https://index.docker.io/v1/'</span>]) {
                            sh <span class="hljs-string">""</span><span class="hljs-string">"
                                echo "</span>Pulling frontend image...<span class="hljs-string">"
                                docker pull <span class="hljs-variable">${env.FRONTEND_IMAGE}</span>:<span class="hljs-variable">${params.IMAGE_TAG}</span>

                                echo "</span>Pulling backend image...<span class="hljs-string">"
                                docker pull <span class="hljs-variable">${env.BACKEND_IMAGE}</span>:<span class="hljs-variable">${params.IMAGE_TAG}</span>

                                echo "</span>Pulling MySQL image...<span class="hljs-string">"
                                docker pull mysql:8.0
                            "</span><span class="hljs-string">""</span>
                        }
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"All images pulled successfully"</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to pull images: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        throw e
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Stop Previous Deployment'</span>) {
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Stopping previous deployment..."</span>
                        sh <span class="hljs-string">""</span><span class="hljs-string">"
                            # Stop and remove existing containers
                            docker-compose down --remove-orphans || true

                            # Clean up any dangling containers
                            docker stop emailgen-frontend emailgen-backend emailgen-db 2&gt;/dev/null || true
                            docker rm emailgen-frontend emailgen-backend emailgen-db 2&gt;/dev/null || true

                            echo "</span>Previous deployment stopped successfully<span class="hljs-string">"
                        "</span><span class="hljs-string">""</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Warning: Failed to stop previous deployment: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"This might be normal if it's the first deployment"</span>
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Deploy Application'</span>) {
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Deploying application..."</span>
                        sh <span class="hljs-string">""</span><span class="hljs-string">"
                            # Deploy using docker-compose
                            docker-compose up -d

                            echo "</span>Deployment initiated successfully<span class="hljs-string">"

                            # Wait for services to start
                            echo "</span>Waiting <span class="hljs-keyword">for</span> services to start...<span class="hljs-string">"
                            sleep 30

                            # Check if containers are running
                            echo "</span>Checking container status...<span class="hljs-string">"
                            docker-compose ps
                        "</span><span class="hljs-string">""</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Deployment failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        throw e
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Health Check'</span>) {
            when {
                expression { !params.SKIP_TESTS }
            }
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Performing health checks..."</span>
                        sh <span class="hljs-string">""</span><span class="hljs-string">"
                            # Wait for applications to be ready
                            echo "</span>Waiting <span class="hljs-keyword">for</span> applications to be ready...<span class="hljs-string">"
                            sleep 45

                            # Check if containers are running
                            echo "</span>=== Container Status ===<span class="hljs-string">"
                            docker ps | grep emailgen || echo "</span>No containers found<span class="hljs-string">"

                            # Check container logs for errors
                            echo "</span>=== Backend Logs ===<span class="hljs-string">"
                            docker logs emailgen-backend --tail 20 || echo "</span>Cannot get backend logs<span class="hljs-string">"

                            echo "</span>=== Frontend Logs ===<span class="hljs-string">"
                            docker logs emailgen-frontend --tail 20 || echo "</span>Cannot get frontend logs<span class="hljs-string">"

                            echo "</span>=== Database Logs ===<span class="hljs-string">"
                            docker logs emailgen-db --tail 20 || echo "</span>Cannot get database logs<span class="hljs-string">"

                            # Debug database connection
                            echo "</span>=== Database Connection Debug ===<span class="hljs-string">"
                            docker exec emailgen-backend printenv | grep MYSQL || echo "</span>No MYSQL env vars found<span class="hljs-string">"

                            echo "</span>=== Testing Database ===<span class="hljs-string">"
                            docker exec emailgen-db mysql -u root -prootpassword123 -e "</span>SHOW DATABASES;<span class="hljs-string">" || echo "</span>Database connection failed<span class="hljs-string">"

                            # Test application endpoints
                            echo "</span>=== Health Check Tests ===<span class="hljs-string">"

                            # Test backend health endpoint
                            echo "</span>Testing backend health at http://localhost:<span class="hljs-variable">${env.BACKEND_PORT}</span>/api/health<span class="hljs-string">"
                            timeout 60 bash -c 'until curl -f http://localhost:<span class="hljs-variable">${env.BACKEND_PORT}</span>/api/health &gt;/dev/null 2&gt;&amp;1; do sleep 5; done' || echo "</span>Backend health check failed<span class="hljs-string">"

                            # Test frontend
                            echo "</span>Testing frontend at http://localhost:<span class="hljs-variable">${env.FRONTEND_PORT}</span><span class="hljs-string">"
                            timeout 60 bash -c 'until curl -f http://localhost:<span class="hljs-variable">${env.FRONTEND_PORT}</span> &gt;/dev/null 2&gt;&amp;1; do sleep 5; done' || echo "</span>Frontend health check failed<span class="hljs-string">"

                            # Test database connection
                            echo "</span>Testing database connection...<span class="hljs-string">"
                            timeout 30 bash -c 'until docker exec emailgen-db mysqladmin ping -h localhost --silent; do sleep 2; done' || echo "</span>Database health check failed<span class="hljs-string">"

                            echo "</span>Health checks completed<span class="hljs-string">"
                        "</span><span class="hljs-string">""</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Health check failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        currentBuild.result = <span class="hljs-string">'UNSTABLE'</span>
                    }
                }
            }
        }

        stage(<span class="hljs-string">'Deployment Verification'</span>) {
            steps {
                script {
                    try {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Verifying deployment..."</span>
                        sh <span class="hljs-string">""</span><span class="hljs-string">"
                            # Final verification
                            echo "</span>=== Final Deployment Status ===<span class="hljs-string">"
                            docker-compose ps

                            # Check if all services are up
                            RUNNING_SERVICES=\$(docker-compose ps --services --filter "</span>status=running<span class="hljs-string">" | wc -l)
                            TOTAL_SERVICES=\$(docker-compose ps --services | wc -l)

                            echo "</span>Running services: \<span class="hljs-variable">$RUNNING_SERVICES</span>/\<span class="hljs-variable">$TOTAL_SERVICES</span><span class="hljs-string">"

                            if [ "</span>\<span class="hljs-variable">$RUNNING_SERVICES</span><span class="hljs-string">" -eq "</span>\<span class="hljs-variable">$TOTAL_SERVICES</span><span class="hljs-string">" ]; then
                                echo "</span>✅ All services are running successfully<span class="hljs-string">"
                            else
                                echo "</span>❌ Some services are not running<span class="hljs-string">"
                                exit 1
                            fi
                        "</span><span class="hljs-string">""</span>
                    } catch (Exception e) {
                        <span class="hljs-built_in">echo</span> <span class="hljs-string">"Deployment verification failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                        throw e
                    }
                }
            }
        }
    }

    post {
        success {
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Deployment Successful ==="</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Image Tag: <span class="hljs-variable">${params.IMAGE_TAG}</span>"</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Frontend URL: http://localhost:<span class="hljs-variable">${env.FRONTEND_PORT}</span>"</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Backend URL: http://localhost:<span class="hljs-variable">${env.BACKEND_PORT}</span>"</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Database Port: <span class="hljs-variable">${env.DB_PORT}</span>"</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Services ==="</span>
            script {
                try {
                    sh <span class="hljs-string">"docker-compose ps"</span>
                } catch (Exception e) {
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Could not display final service status"</span>
                }
            }
        }
        always {
            script {
                try {
                    // Clean up unused images
                    sh <span class="hljs-string">""</span><span class="hljs-string">"
                        docker image prune -f
                        echo "</span>Cleaned up unused images<span class="hljs-string">"
                    "</span><span class="hljs-string">""</span>
                } catch (Exception e) {
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Cleanup failed: <span class="hljs-variable">${e.getMessage()}</span>"</span>
                }
            }
        }
        failure {
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Deployment Failed ==="</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Image Tag: <span class="hljs-variable">${params.IMAGE_TAG}</span>"</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Check the logs above for details"</span>

            script {
                try {
                    // Show container logs on failure
                    sh <span class="hljs-string">""</span><span class="hljs-string">"
                        echo "</span>=== Container Logs on Failure ===<span class="hljs-string">"
                        docker logs emailgen-backend --tail 30 2&gt;/dev/null || echo "</span>No backend logs<span class="hljs-string">"
                        docker logs emailgen-frontend --tail 30 2&gt;/dev/null || echo "</span>No frontend logs<span class="hljs-string">"
                        docker logs emailgen-db --tail 30 2&gt;/dev/null || echo "</span>No database logs<span class="hljs-string">"
                    "</span><span class="hljs-string">""</span>
                } catch (Exception e) {
                    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Could not retrieve container logs"</span>
                }
            }
        }
        unstable {
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Deployment Completed with Issues ==="</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"The application was deployed but some health checks failed"</span>
            <span class="hljs-built_in">echo</span> <span class="hljs-string">"Please check the application manually"</span>
        }
    }
}
</code></pre>
<h3 id="heading-instructions-to-create-cd-pipeline"><strong>Instructions to Create CD Pipeline</strong></h3>
<ol>
<li><p><strong>Login to Jenkins dashboard</strong></p>
</li>
<li><p>Click <strong>“New Item”</strong></p>
</li>
<li><p>Enter name: <code>email-gen-cd</code></p>
</li>
<li><p>Select <strong>Pipeline</strong>, click <strong>OK</strong></p>
</li>
<li><p>Scroll down to <strong>Pipeline section</strong></p>
</li>
<li><p>Paste your <strong>provided pipeline Groovy script</strong> (above)</p>
</li>
<li><p>Click <strong>“Save”</strong></p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752332733698/4ea55e36-5edf-4ae4-b75a-266e306ee0e1.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-final-verification"><strong>Final Verification</strong></h4>
<ol>
<li><p>Access application:</p>
<ul>
<li><p><strong>Webapp URL:</strong> http://&lt;server-ip&gt;:5000</p>
</li>
<li><p><strong>Backend URL:</strong> http://&lt;server-ip&gt;:5001</p>
</li>
</ul>
</li>
<li><p>Check running containers:</p>
<pre><code class="lang-bash"> docker ps
</code></pre>
</li>
</ol>
<hr />
<h2 id="heading-step-8-configure-bit-bucket-web-hook-with-jenkins">✅ <strong>Step 8: Configure Bit bucket Web-hook with Jenkins</strong></h2>
<p>Setting up a webhook enables <strong>automatic triggering of pipelines</strong> when changes are pushed to the repository. In this project, pushing or updating the <code>main</code> branch:</p>
<ul>
<li><p><strong>Runs the CI pipeline automatically</strong></p>
</li>
<li><p><strong>After CI completion, triggers the CD pipeline</strong></p>
</li>
<li><p><strong>Deploys the updated application seamlessly</strong></p>
</li>
</ul>
<h4 id="heading-instructions"><strong>Instructions</strong></h4>
<ol>
<li><p>Go to your <strong>Bitbucket repository</strong>.</p>
</li>
<li><p>Click on <strong>Repository settings</strong> in the sidebar.</p>
</li>
<li><p>Under <strong>Workflow</strong>, click <strong>Webhooks</strong>.</p>
</li>
<li><p>Click <strong>“Add Webhook”</strong>.</p>
</li>
<li><p>Configure the webhook with the following:</p>
</li>
</ol>
<ul>
<li><p><strong>Title:</strong> Jenkins CI/CD Trigger</p>
</li>
<li><p><strong>URL:</strong> http://:8080/bitbucket-hook/</p>
</li>
<li><p><strong>Triggers:</strong> Repository push</p>
</li>
</ul>
<ol start="6">
<li>Save the webhook configuration.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752333934907/68ed6159-d947-4c9e-9631-461b9e5331a9.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-verify-webhook-functionality"><strong>Verify Webhook Functionality</strong></h4>
<p>If the webhook is configured correctly:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752334007470/0cfc686b-36ef-4be0-b946-3e73637dfa42.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>A <strong>successful POST request status (200)</strong> will display in Bitbucket webhook page after pushing code.</p>
</li>
<li><p>Jenkins will show a new <strong>CI pipeline build triggered automatically</strong>.</p>
</li>
<li><p>Upon CI pipeline success, the <strong>CD pipeline will start automatically</strong>, deploying the application.</p>
</li>
</ul>
<hr />
<h2 id="heading-step-9-update-repository-with-new-application-email-subject-generator-gen-ai-app">✅ <strong>Step 9: Update</strong> Repository with New <strong>Application – Email Subject Generator (Gen AI App)</strong></h2>
<p>After verifying that the <strong>CI/CD pipeline works correctly</strong> with your basic multi-tier setup, we now update the repository with the <strong>actual production application code</strong> – the <strong>Email Subject Generator (Gen AI App)</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752334702860/fb12d19b-fd51-4a0c-9270-49f89ab09813.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-whats-being-deployed-now">🚀 <strong>What’s Being Deployed Now?</strong></h4>
<p>This new version of the app uses <strong>Generative AI techniques</strong> to analyze the scenario input and generate a <strong>suitable subject line for emails</strong>.</p>
<h4 id="heading-application-features"><strong>Application Features</strong></h4>
<ul>
<li><p><strong>Generate Email Subject</strong></p>
</li>
<li><p><strong>View History</strong></p>
</li>
</ul>
<hr />
<h2 id="heading-common-errors-amp-fixes">❗Common Errors &amp; Fixes</h2>
<p>Below is a list of the key issues encountered during the CI/CD setup and deployment of the <strong>Email Subject Generator (Gen AI App)</strong>, along with the solutions applied.</p>
<h4 id="heading-error-1-1045-access-denied-for-user-sachindulocalhosthttplocalhost">❌ <strong>Error 1: (1045) Access denied for user 'sachindu'@'</strong><a target="_blank" href="http://localhost"><strong>localhost</strong></a><strong>'</strong></h4>
<p>🔑 <strong>Cause:</strong> User does not exist or lacks privileges.</p>
<p>✅ <strong>Solution A – Create New User</strong></p>
<pre><code class="lang-sql">sudo mysql -u root -p

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> <span class="hljs-string">'sachindu'</span>@<span class="hljs-string">'localhost'</span> <span class="hljs-keyword">IDENTIFIED</span> <span class="hljs-keyword">BY</span> <span class="hljs-string">'your_password'</span>;
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">ON</span> email_generator.* <span class="hljs-keyword">TO</span> <span class="hljs-string">'sachindu'</span>@<span class="hljs-string">'localhost'</span>;
<span class="hljs-keyword">FLUSH</span> <span class="hljs-keyword">PRIVILEGES</span>;
</code></pre>
<p>✅ <strong>Update .env</strong></p>
<pre><code class="lang-bash">MYSQL_USER=sachindu
MYSQL_PASSWORD=your_password
</code></pre>
<p>✅ <strong>Update app.py database configuration accordingly.</strong></p>
<hr />
<p>✅ <strong>Solution B – Use Root User</strong></p>
<pre><code class="lang-sql">sudo mysql -u root -p

<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">ON</span> email_generator.* <span class="hljs-keyword">TO</span> <span class="hljs-string">'root'</span>@<span class="hljs-string">'localhost'</span>;
<span class="hljs-keyword">FLUSH</span> <span class="hljs-keyword">PRIVILEGES</span>;
</code></pre>
<p>✅ <strong>Update .env</strong></p>
<pre><code class="lang-bash">MYSQL_USER=root
MYSQL_PASSWORD=rootpassword123
</code></pre>
<hr />
<h4 id="heading-error-2-mysql-authentication-issues-cachingsha2password">❌ <strong>Error 2: MySQL Authentication Issues (caching_sha2_password)</strong></h4>
<p>🔑 <strong>Cause:</strong> MySQL 8.0 uses incompatible default auth plugin.</p>
<p>✅ <strong>Solution:</strong> Ensure .env and docker-compose use correct root credentials only. Use <code>mysql_native_password</code> if needed.</p>
<hr />
<h4 id="heading-error-3-cicd-container-running-error-backend-cant-connect-to-db">❌ <strong>Error 3: CI/CD – Container Running Error (Backend can't connect to DB)</strong></h4>
<p>🔑 <strong>Cause:</strong> Database not correctly connected with backend container.</p>
<p>✅ <strong>Solution:</strong></p>
<ul>
<li><p>Verify <strong>MYSQL_HOST, USER, PASSWORD, DATABASE</strong> in .env.</p>
</li>
<li><p>Ensure <strong>database container is healthy and accessible</strong> before backend starts.</p>
</li>
<li><p>Confirm <strong>docker-compose depends_on</strong> is configured for backend -&gt; db.</p>
</li>
</ul>
<hr />
<h4 id="heading-error-4-sonarqube-installation-issues">❌ <strong>Error 4: SonarQube Installation Issues</strong></h4>
<p>🔑 <strong>Cause:</strong> Online installer fails or times out in some environments.</p>
<p>✅ <strong>Solution:</strong> Use <strong>offline local testing method</strong>, such as:</p>
<ul>
<li><p>Download and extract SonarQube manually.</p>
</li>
<li><p>Start locally using:</p>
</li>
</ul>
<pre><code class="lang-bash">./bin/linux-x86-64/sonar.sh start
</code></pre>
<ul>
<li>Access via <strong>localhost:9000</strong> for analysis testing.</li>
</ul>
<hr />
<h3 id="heading-feedback-amp-suggestions">💬 <strong>Feedback &amp; Suggestions</strong></h3>
<p>I always welcome your <strong>feedback and suggestions</strong> to improve these projects and pipelines further.<br />✨ <strong>Love to hear your thoughts!</strong></p>
]]></content:encoded></item><item><title><![CDATA[How to Use Groq in Development and Testing Projects: A Complete Guide for Zero-Cost AI Integration]]></title><description><![CDATA[Introduction
Are you looking to integrate powerful AI capabilities into your development and testing projects without breaking the bank? Groq offers an excellent solution for developers who want to leverage fast language models in their applications ...]]></description><link>https://blog.sachindu.me/how-to-use-groq-in-development-and-testing-projects-a-complete-guide-for-zero-cost-ai-integration</link><guid isPermaLink="true">https://blog.sachindu.me/how-to-use-groq-in-development-and-testing-projects-a-complete-guide-for-zero-cost-ai-integration</guid><category><![CDATA[groq]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[APIs]]></category><category><![CDATA[zerocost]]></category><category><![CDATA[devtools]]></category><category><![CDATA[large language models]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Thu, 10 Jul 2025 16:17:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752162497905/7a33b06d-845e-4f2a-b98e-d23b5c295344.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Are you looking to integrate powerful AI capabilities into your development and testing projects without breaking the bank? Groq offers an excellent solution for developers who want to leverage fast language models in their applications at zero cost. In this comprehensive guide, we'll walk you through everything you need to know about using Groq in your development workflow.</p>
<h2 id="heading-what-is-groq">What is Groq?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752163063598/490e475f-7af9-41c3-8b1b-1ec4fe1b21d2.png" alt class="image--center mx-auto" /></p>
<p>Groq is a cutting-edge AI inference platform that provides <strong>lightning-fast language model</strong> processing. It's designed to deliver exceptional performance for AI applications, making it an ideal choice for developers working on chatbots, content generation, code analysis, and testing automation.</p>
<h2 id="heading-getting-started-with-groq">Getting Started with Groq</h2>
<h3 id="heading-step-1-creating-your-groq-account">Step 1: Creating Your Groq Account</h3>
<p>First, you'll need to access the Groq platform:</p>
<ol>
<li><p><strong>Visit the Official Website</strong>: Navigate to <a target="_blank" href="https://groq.com">groq.com</a></p>
</li>
<li><p><strong>Account Creation</strong>: You'll be prompted to create an account or log in using your existing Google or GitHub credentials</p>
</li>
<li><p><strong>Choose Your Preferred Method</strong>: Select either Google or GitHub for quick authentication</p>
</li>
</ol>
<h3 id="heading-step-2-exploring-the-groq-dashboard">Step 2: Exploring the Groq Dashboard</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752163200795/9b721c02-e75a-4bd5-8c5a-2aad26b0bf23.png" alt class="image--center mx-auto" /></p>
<p>Once logged in, you'll see the Groq dashboard with several key features:</p>
<ol>
<li><p><strong>User Message Testing</strong>: Test the AI capabilities by entering any prompt message to see how the model responds</p>
</li>
<li><p><strong>Template Script Access</strong>: Use the dropdown menu to access pre-built template scripts</p>
</li>
<li><p><strong>Model Selection</strong>: Choose from various available models, keeping in mind that free access may have different limitations depending on the model selected</p>
</li>
</ol>
<h3 id="heading-step-3-creating-your-groq-api-key">Step 3: Creating Your Groq API Key</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752163495574/e1722ba8-9839-4326-b457-cc3e49a0964c.png" alt class="image--center mx-auto" /></p>
<p>To use Groq in your development projects, you'll need to generate an API key:</p>
<ol>
<li><p><strong>Navigate to API Keys</strong>: From the dashboard, locate and click on the "API Keys" section</p>
</li>
<li><p><strong>Create New Key</strong>: Click the "Create API Key" button</p>
</li>
<li><p><strong>Name Your Key</strong>: Give your API key a preferred name (e.g., "Development Project" or "Testing Environment")</p>
</li>
<li><p><strong>Generate Key</strong>: Click "Submit" to create your API key</p>
</li>
<li><p><strong>Save Your Secret Key</strong>: You'll receive a secret key - <strong>copy and save it immediately</strong> as you won't be able to view it again</p>
</li>
</ol>
<p><mark>⚠️ </mark> <strong><mark>Important Security Note</mark></strong><mark>: Store your API key securely and never share it publicly or commit it to version control systems.</mark></p>
<hr />
<h2 id="heading-setting-up-groq-in-your-development-project">Setting Up Groq in Your Development Project</h2>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>The following demonstration focuses on creating a Python project, but Groq also supports JavaScript, CURL, and JSON implementations. Before integrating Groq into your project, ensure you have:</p>
<p><strong>Note</strong>: Check the official Groq documentation for language-specific examples.</p>
<p>Here the below demonstration given by assuming creating a python project. It can use as JavaScript, curl, JSON Before integrating Groq into your project, ensure you have:</p>
<ul>
<li><p>Python installed on your system</p>
</li>
<li><p>A valid Groq API key</p>
</li>
<li><p>Basic understanding of Python programming</p>
</li>
</ul>
<h3 id="heading-step-1-installation">Step 1: Installation</h3>
<p>Install the Groq library using pip:</p>
<pre><code class="lang-bash">pip install groq
</code></pre>
<h3 id="heading-step-2-basic-implementation">Step 2: Basic Implementation</h3>
<p>Here's the basic template provided by Groq for Python projects:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> groq <span class="hljs-keyword">import</span> Groq

client = Groq(
    api_key=os.environ.get(<span class="hljs-string">"GROQ_API_KEY"</span>),
)

chat_completion = client.chat.completions.create(
    messages=[
        {
            <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
            <span class="hljs-string">"content"</span>: <span class="hljs-string">"Explain the importance of fast language models"</span>,
        }
    ],
    model=<span class="hljs-string">"llama-3.3-70b-versatile"</span>,
)

print(chat_completion.choices[<span class="hljs-number">0</span>].message.content)
</code></pre>
<h3 id="heading-step-3-configuration">Step 3: Configuration</h3>
<p><strong>Important Configuration Steps:</strong></p>
<ol>
<li><p><strong>Set Your API Key</strong>: Ensure your <code>GROQ_API_KEY</code> environment variable is properly configured</p>
</li>
<li><p><strong>Customize Content</strong>: Replace the content field with your specific prompt message</p>
</li>
<li><p><strong>Choose Your Model</strong>: Select the appropriate model based on your project requirements</p>
</li>
</ol>
<hr />
<h2 id="heading-practical-implementation-examples">Practical Implementation Examples</h2>
<h3 id="heading-example-1-content-generation-for-testing">Example 1: Content Generation for Testing</h3>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> groq <span class="hljs-keyword">import</span> Groq

client = Groq(api_key=os.environ.get(<span class="hljs-string">"GROQ_API_KEY"</span>))

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_test_data</span>():</span>
    chat_completion = client.chat.completions.create(
        messages=[
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
                <span class="hljs-string">"content"</span>: <span class="hljs-string">"Generate 5 sample user profiles for testing an e-commerce application"</span>,
            }
        ],
        model=<span class="hljs-string">"llama-3.3-70b-versatile"</span>,
    )
    <span class="hljs-keyword">return</span> chat_completion.choices[<span class="hljs-number">0</span>].message.content

<span class="hljs-comment"># Use in your testing workflow</span>
test_data = generate_test_data()
print(test_data)
</code></pre>
<h3 id="heading-example-2-code-review-assistant">Example 2: Code Review Assistant</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">code_review_assistant</span>(<span class="hljs-params">code_snippet</span>):</span>
    chat_completion = client.chat.completions.create(
        messages=[
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
                <span class="hljs-string">"content"</span>: <span class="hljs-string">f"Review this code for potential improvements and bugs: <span class="hljs-subst">{code_snippet}</span>"</span>,
            }
        ],
        model=<span class="hljs-string">"llama-3.3-70b-versatile"</span>,
    )
    <span class="hljs-keyword">return</span> chat_completion.choices[<span class="hljs-number">0</span>].message.content
</code></pre>
<h2 id="heading-advantages-of-using-groq">Advantages of Using Groq</h2>
<ol>
<li><p><strong>Zero Cost</strong>: Perfect for development and testing phases</p>
</li>
<li><p><strong>Fast Processing</strong>: Lightning-fast inference speeds</p>
</li>
<li><p><strong>Multiple Models</strong>: Access to various language models</p>
</li>
<li><p><strong>Easy Integration</strong>: Simple API integration</p>
</li>
<li><p><strong>Comprehensive Documentation</strong>: Well-documented APIs and examples</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Groq provides an excellent opportunity for developers to integrate powerful AI capabilities into their projects without any upfront costs. By following this guide, you can quickly get started with Groq and begin leveraging its capabilities in your development and testing workflows.</p>
<p>Whether you're building automated testing tools, generating content, or creating AI-powered applications, Groq offers the speed and reliability you need to succeed.</p>
<h2 id="heading-whats-next">What's Next?</h2>
<p>Start by creating your Groq account and experimenting with the basic examples provided in this guide. As you become more comfortable with the platform, explore advanced features and integration possibilities.</p>
<hr />
<p><strong>Have you used Groq in your development projects?</strong> Share your experiences and suggestions in the comments below! Your feedback helps the community learn and grow together.</p>
]]></content:encoded></item><item><title><![CDATA[How to Set Up a Home-Lab: Upgrade Your Old Laptop to a Cloud Server]]></title><description><![CDATA[Ever wanted to get hands-on DevOps experience without spending a fortune? I turned my old laptop into a fully functional home server that I can access from anywhere — and you can too! Here's how I did]]></description><link>https://blog.sachindu.me/setup-homelab-cloud-server</link><guid isPermaLink="true">https://blog.sachindu.me/setup-homelab-cloud-server</guid><category><![CDATA[Homelab]]></category><category><![CDATA[Devops]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[Ubuntu 24.04]]></category><category><![CDATA[ubuntu-server]]></category><category><![CDATA[Linux]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[vpn]]></category><category><![CDATA[ssh]]></category><category><![CDATA[server]]></category><category><![CDATA[networking]]></category><category><![CDATA[sysadmin]]></category><category><![CDATA[automation]]></category><category><![CDATA[Cloud Computing]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Sat, 28 Jun 2025 10:19:11 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/6da263c1-5e3d-4333-9b24-53b07705694d.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ever wanted to get hands-on DevOps experience without spending a fortune? I turned my old laptop into a fully functional home server that I can access from anywhere — and you can too! Here's how I did it, step by step.</p>
<hr />
<h2>Why Build a Home-lab?</h2>
<ul>
<li><p>Learn Linux system administration and networking</p>
</li>
<li><p>Gain practical DevOps skills (beyond theory!)</p>
</li>
<li><p>Host and experiment with personal projects</p>
</li>
<li><p>Control your environment, privately and securely</p>
</li>
</ul>
<hr />
<h2>What I Used</h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>💻 Hardware</td>
<td>eME730 laptop (Intel i3 M 350, 4GB RAM)</td>
</tr>
<tr>
<td>🧊 Cooling</td>
<td>Custom plastic enclosure for ventilation</td>
</tr>
<tr>
<td>🐧 OS</td>
<td>Ubuntu Server 24.04.2 LTS (lightweight, stable)</td>
</tr>
<tr>
<td>🌐 Connectivity</td>
<td>Wired Ethernet + SSH</td>
</tr>
<tr>
<td>🔐 Remote Access</td>
<td>Tailscale VPN</td>
</tr>
</tbody></table>
<hr />
<h2>Step-by-Step Setup</h2>
<h3>1. Clean the Old Laptop</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751097991555/e4e42fc6-75c2-4f1e-9cf7-6d7e1eb49071.jpeg" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>Removed unnecessary parts like the broken display</p>
</li>
<li><p>Replaced thermal paste and ensured good airflow</p>
</li>
<li><p>Built a custom enclosure for better cooling (Here I used old plastic box)</p>
</li>
</ul>
<h3>2. Install Ubuntu Server</h3>
<ul>
<li><p>Download Ubuntu Server 24.04.2 : <a href="https://ubuntu.com/download/server">https://ubuntu.com/download/server</a></p>
  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751098496264/732ad2b4-aa67-4a94-81e8-0860d1d8e80d.png" alt="" style="display:block;margin:0 auto" />
  </li>
<li><p>Create a bootable USB with <strong>Balena Etcher</strong> or <strong>Rufus</strong></p>
<ul>
<li><p>Rufus download Link: <a href="https://rufus.ie/en/">https://rufus.ie/en/</a></p>
</li>
<li><p>Balena Etcher download Link: <a href="https://etcher.balena.io/">https://etcher.balena.io/</a></p>
</li>
</ul>
</li>
<li><p>Install using minimal setup <strong>(no GUI)</strong> to save resources</p>
  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751099587840/abf45aa8-a2b7-440c-bd49-9e07f13b0a0a.jpeg" alt="" style="display:block;margin:0 auto" /></li>
</ul>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=K2m52F0S2w8">https://www.youtube.com/watch?v=K2m52F0S2w8</a></p>
### 3\. Network Configuration &amp; SSH Setup

<ul>
<li><p>Connect the laptop to your router via <strong>Ethernet</strong> for a stable connection.</p>
</li>
<li><p>Install basic networking tools:</p>
<pre><code class="language-bash">sudo apt install net-tools
</code></pre>
<p>  Use <code>ifconfig</code> to check your local IP address:</p>
<pre><code class="language-bash">ifconfig
</code></pre>
</li>
</ul>
<h4>✅ Verify SSH Access</h4>
<p>Try accessing your server from another device:</p>
<pre><code class="language-bash">ssh username@your-local-ip
</code></pre>
<ul>
<li><p>Replace <code>username</code> and <code>your-local-ip</code> with your actual credentials (you can find the IP from <code>ifconfig</code> under <code>inet</code>).</p>
</li>
<li><p>If it is successful, show like below.</p>
  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751103811701/1ad1cd93-dbfe-4904-9056-e75a31074082.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
<h4>❗ Troubleshooting SSH</h4>
<p>If SSH is <strong>not working</strong>, follow these steps:</p>
<ol>
<li><p><strong>Check if SSH is installed:</strong></p>
<pre><code class="language-bash">dpkg -l | grep openssh-server
</code></pre>
</li>
<li><p><strong>If not installed, install it:</strong></p>
<pre><code class="language-bash">sudo apt install openssh-server
</code></pre>
</li>
<li><p><strong>Start and enable the SSH service:</strong></p>
<pre><code class="language-bash">sudo systemctl start ssh
sudo systemctl enable ssh
</code></pre>
</li>
<li><p><strong>Check SSH status:</strong></p>
<pre><code class="language-bash">sudo systemctl status ssh
</code></pre>
</li>
</ol>
<blockquote>
<p>🔒 Tip: Allow SSH in your firewall (if UFW is enabled):</p>
</blockquote>
<pre><code class="language-bash">sudo ufw allow ssh
</code></pre>
<p>Absolutely! Here's the updated <strong>Point 4 and 5</strong> in markdown format, with the additional notes on remote access, account authentication, and accessing from other devices:</p>
<hr />
<h3>4. Set Up Remote Access with Tailscale</h3>
<ul>
<li><p><strong>Install Tailscale on the server machine</strong> (a simple, secure VPN for remote access):</p>
<pre><code class="language-bash">curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
</code></pre>
</li>
<li><p>Sign in using your Tailscale account to register the server : <a href="https://tailscale.com/">https://tailscale.com/</a></p>
  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751104590491/b0257189-7daa-4892-9182-007424544a68.png" alt="" style="display:block;margin:0 auto" />
  </li>
<li><p><strong><mark>Important:</mark></strong><mark><br />When setting up, use the </mark> <strong><mark>same Tailscale account</mark></strong> <mark> across:</mark></p>
<ul>
<li><p><mark>Your </mark> <strong><mark>server (homelab laptop)</mark></strong></p>
</li>
<li><p><mark>Any </mark> <strong><mark>other device</mark></strong> <mark> (e.g., personal laptop, phone) you use to access it</mark></p>
</li>
</ul>
</li>
</ul>
<p>✅ <strong>Tailscale Tip:</strong><br />If the setup is correct, you will see a <strong>“Connected”</strong> status with a <strong>green dot</strong> next to your device in the Tailscale dashboard when it’s online.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751105298949/42672964-4799-41b1-b00c-83afc7893697.jpeg" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>After setup, access your homelab <a href="https://login.tailscale.com/admin/machines">from anywhere:</a></p>
<pre><code class="language-bash">ssh username@your-tailscale-ip
</code></pre>
<blockquote>
<p>Find the Tailscale IP by running:</p>
<pre><code class="language-bash">tailscale ip -4
</code></pre>
</blockquote>
</li>
</ul>
<h3>5. Basic Server Configuration &amp; Security</h3>
<ul>
<li><p>Update the system:</p>
<pre><code class="language-bash">sudo apt update &amp;&amp; sudo apt upgrade
</code></pre>
</li>
<li><p>Install essential tools:</p>
<pre><code class="language-bash">sudo apt install htop git ufw fail2ban
</code></pre>
</li>
<li><p>Set up firewall with UFW:</p>
<pre><code class="language-bash">sudo ufw allow OpenSSH
sudo ufw enable
</code></pre>
</li>
<li><p>Optional but recommended: Enable fail2ban to protect against brute-force attacks:</p>
<pre><code class="language-bash">sudo systemctl enable fail2ban
sudo systemctl start fail2ban
</code></pre>
</li>
</ul>
<h2>💬 Got a Better Way?</h2>
<p>If you’ve set up your own home-lab differently or have suggestions to improve this setup, I’d love to hear from you!</p>
<blockquote>
<p>💡 <strong>Share your thoughts:</strong></p>
<ul>
<li><p>Did you use another remote access method like <strong>OpenVPN</strong>, or <strong>WireGuard</strong>?</p>
</li>
<li><p>Have tips to optimize performance on low-spec hardware?</p>
</li>
<li><p>Got questions or need help with a similar setup?</p>
</li>
</ul>
</blockquote>
<p>👇 <strong>Drop a comment below -</strong> let’s learn from each other!</p>
]]></content:encoded></item><item><title><![CDATA[SEO Content Optimizer Tool]]></title><description><![CDATA[This project is a Node JS based SEO Content Optimizer Tool designed to analyze and enhance web content for better visibility on search engines. The tool provides keyword analysis, readability checks, ]]></description><link>https://blog.sachindu.me/seo-content-optimizer-tool</link><guid isPermaLink="true">https://blog.sachindu.me/seo-content-optimizer-tool</guid><category><![CDATA[Devops]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[Jenkins]]></category><category><![CDATA[jenkins pipeline]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Sachindu Malshan]]></dc:creator><pubDate>Thu, 26 Jun 2025 17:42:30 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/685b63b17e7d46cc40b0aa24/f206f757-ccc9-47c5-999d-6f0710db6043.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750958074923/daf2baee-8025-4355-a92b-382a8607020f.png" alt="" />

<p>This project is a <strong>Node JS based SEO Content Optimizer Tool</strong> designed to analyze and enhance web content for better visibility on search engines. The tool provides keyword analysis, readability checks, and metadata suggestions to help developers and marketers create high-ranking content.</p>
<p>To streamline development and deployment, we implemented a <strong>fully automated CI/CD pipeline using Jenkins</strong>. The source code is hosted on <strong>GitHub</strong>, and every push to the repository triggers an automated workflow that:</p>
<ol>
<li><p><strong>Builds and tests</strong> the Node.js application using Jenkins.</p>
</li>
<li><p><strong>Packages</strong> the app into a Docker image.</p>
</li>
<li><p><strong>Pushes</strong> the image to <strong>Docker Hub</strong>.</p>
</li>
<li><p>Optionally deploys the image to a target server or cloud platform.</p>
</li>
</ol>
<p><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">GitHub Repository Link</mark></strong>: <a href="https://github.com/sachindumalshan/node-appp.git">https://github.com/sachindumalshan/node-appp.git</a></p>
<h2>Step 1: Prerequisites</h2>
<hr />
<h4>Install Docker (Original)</h4>
<pre><code class="language-bash"># Install docker original
sudo apt install docker.io -y

# Verify by running hello-world container
sudo docker run hello-world
</code></pre>
<h4>Native Jenkins Installation</h4>
<p>Instead of Docker-based Jenkins, install Jenkins natively:</p>
<pre><code class="language-bash">sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install openjdk-17-jdk -y

curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc &gt; /dev/null

sudo apt update
sudo apt install jenkins -y

sudo systemctl start jenkins
sudo systemctl enable jenkins
sudo ufw allow 8080
</code></pre>
<blockquote>
<p>⚠️ <strong>Note:</strong> Installing Java JDK 17 separately is <strong>not sufficient</strong> for the latest Jenkins version. It recommends using <strong>Java JDK 21</strong>.</p>
</blockquote>
<h4>Install Git</h4>
<pre><code class="language-bash">sudo apt install git
</code></pre>
<h2>Step 2: Setting Up Jenkins</h2>
<hr />
<h4>Access Jenkins from Another Computer</h4>
<ul>
<li>Open the URL below in your browser. It will display the Jenkins startup interface and prompt you to enter the administrator password. Use the following command to retrieve the password, then enter it in the prompt to log into Jenkins.</li>
</ul>
<pre><code class="language-bash"># Use 'ifconfig' to get the server IP address
# Ex: http://192.168.8.129:8080/

http://&lt;server-ip&gt;:8080
</code></pre>
<blockquote>
<p>Get the Password:</p>
</blockquote>
<pre><code class="language-bash">sudo cat /var/lib/jenkins/secrets/initialAdminPassword

# If the above command doesn't work, try:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
</code></pre>
<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">If you skip adding user details during setup, you can log in using the default </mark> <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">admin</mark></strong> <mark class="bg-yellow-200 dark:bg-yellow-500/30">account.</mark></p>
<ul>
<li><p><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">Username:</mark></strong> <mark class="bg-yellow-200 dark:bg-yellow-500/30">admin</mark></p>
</li>
<li><p><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">Password:</mark></strong> <mark class="bg-yellow-200 dark:bg-yellow-500/30">Use the password generated using the command shown above.</mark></p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750958844684/288bb29b-9d21-4921-b496-77203e0ab338.png" alt="" />

<p>Then, install the necessary libraries and complete the configurations. You will end up on the Jenkins dashboard.</p>
<h4>Install Necessary Plugins</h4>
<p>Go to: <code>Manage Jenkins &gt; Manage Plugins</code></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750958922532/748d6288-1268-41b4-a792-73b46608500d.png" alt="" />

<p>Search and install the plugins as needed. Install essential plugins such as:</p>
<ul>
<li><p>Git</p>
</li>
<li><p>Pipeline</p>
</li>
<li><p>NodeJS</p>
</li>
<li><p>Other relevant tools</p>
</li>
</ul>
<h4>Configure Build Tools</h4>
<h6>1. JDK configure</h6>
<pre><code class="language-bash"># Name
JDK-17

# Set the JDK path as:
/usr/lib/jvm/java-17-openjdk-amd64
</code></pre>
<pre><code class="language-bash"># check java jdk installation path
sudo update-alternatives --config java
</code></pre>
<p>If not found, Update and install OpenJDK 17:</p>
<pre><code class="language-bash">sudo apt update
sudo apt install openjdk-17-jdk -y
</code></pre>
<h6>2. Git configure</h6>
<pre><code class="language-bash"># Name
Default 

# Set the Git path as:
/usr/bin/git

# To check git path
which git
</code></pre>
<h6>3. Node JS configure</h6>
<ul>
<li><p>Name - Node JS-18</p>
</li>
<li><p>✅ check the box <strong>Install automatically</strong></p>
</li>
<li><p>Select the Node JS version</p>
</li>
</ul>
<h6>4. Docker configure</h6>
<pre><code class="language-bash"># Name
Docker

# Set the JDK path as:
/usr/bin/docker
</code></pre>
<h2>Step 3: Prepare the Application</h2>
<hr />
<h4>Create Sample Node.js Application</h4>
<pre><code class="language-bash"># Create a new directory for your Node.js application
mkdir my-node-app

# Move into the newly created directory
cd my-node-app

# Initialize a new Node.js project with default settings
# This creates a package.json file with default values
npm init -y
</code></pre>
<p>Create <code>index.js</code>:</p>
<pre><code class="language-js">import http from "http";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const server = http.createServer((req, res) =&gt; {
  const filePath = path.join(__dirname, "message.txt");

  fs.readFile(filePath, "utf8", (err, data) =&gt; {
    if (err) {
      res.writeHead(500, { "Content-Type": "text/plain" });
      res.end("Error reading file");
    } else {
      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end(data);
    }
  });
});

server.listen(3000, () =&gt; {
  console.log("Server is running on port 3000");
});
</code></pre>
<p>Create <code>message.txt</code>:</p>
<pre><code class="language-plaintext">Hello, this is a Node.js application without Express!
</code></pre>
<p>Run:</p>
<pre><code class="language-bash">node index.js
</code></pre>
<blockquote>
<p>❗ <strong>ERROR:</strong> To load an ES module, set <code>"type": "module"</code> in <code>package.json</code>.</p>
</blockquote>
<h5>Fix</h5>
<p>Add to <code>package.json</code>:</p>
<pre><code class="language-json">"type": "module",
</code></pre>
<h2>Step 4: Push to GitHub</h2>
<hr />
<pre><code class="language-bash"># Initialize Git in the Project Folder
git init

# Create a `.gitignore` File
echo "node_modules/" &gt;&gt; .gitignore
echo ".env" &gt;&gt; .gitignore

# Track Changes and Commit
git add .
git commit -m "Initial commit"

# Connect Local Repository to Remote
git remote add origin https://github.com/sachindumalshan/node-appp.git

# Push Code to the Main Branch
git branch -M main
git push -u origin main
</code></pre>
<blockquote>
<p>❗ <strong>GitHub Push Error:</strong> Instead of GitHub password, use <strong>Access Token</strong>.</p>
</blockquote>
<p>Create one from:<br /><a href="https://github.com/settings/tokens">https://github.com/settings/tokens</a> → Generate classic token with full access.</p>
<h2>Step 5: Jenkins Pipeline Configuration</h2>
<hr />
<p>Create new pipeline in Jenkins:</p>
<pre><code class="language-bash">pipeline {
    agent any
    
    // GitHub webhook trigger
    triggers {
        githubPush()
    }
    
    environment {
        DOCKER_REGISTRY = 'docker.io'
        DOCKER_IMAGE = 'example/node-app'
        DOCKER_TAG = "${BUILD_NUMBER}"
        DOCKER_CREDENTIALS_ID = 'docker-hub-credentials'
        APP_PORT = '5000'
        CONTAINER_NAME = 'node-app-container'
    }
    
    stages {
        stage('Clone Repository') {
            steps {
                // Use the GitHub token for authentication
                checkout scmGit(
                    branches: [[name: '*/main']], 
                    extensions: [], 
                    userRemoteConfigs: [[
                        credentialsId: 'githubtoken', 
                        url: 'https://github.com/sachindumalshan/node-appp.git'
                    ]]
                )
            }
        }
        
        stage('Build') {
            steps {
                sh 'echo "Building the application..."'
                sh 'ls -la'  // Show what files we have
            }
        }
        
        stage('Test') {
            steps {
                sh 'echo "Running tests..."'
            }
        }
        
        stage('Code Quality') {
            steps {
                sh 'echo "Running code quality checks..."'
                // Add linting, security scanning, etc.
                // sh 'npm run lint || echo "No lint script found, skipping..."'
            }
        }
        
        stage('Docker Build') {
            steps {
                script {
                    // Build with both latest and build number tags
                    echo "Building Docker image: \({DOCKER_IMAGE}:\){DOCKER_TAG}"
                    sh "docker build -t \({DOCKER_IMAGE}:\){DOCKER_TAG} -t ${DOCKER_IMAGE}:latest ."
                }
            }
        }
        
        stage('Docker Push') {
            steps {
                script {
                        sh "docker login -u DockerHub_username -p DockerHub_Password"
                        sh "docker push \({DOCKER_IMAGE}:\){DOCKER_TAG}"
                        sh "docker push ${DOCKER_IMAGE}:latest"
                        sh "docker logout"
                }
            }
        }
        
        stage('Pre-Deploy Cleanup') {
            steps {
                script {
                    echo "🧹 Starting cleanup process..."
                    
                    sh '''
                        set +e  # Don't exit on errors - we want to continue cleanup
                        
                        echo "=== Cleanup Process Started ==="
                        
                        # Method 1: Direct container removal (most reliable)
                        echo "1. Removing container: ${CONTAINER_NAME}"
                        
                        # Check if container exists (running or stopped)
                        if docker ps -a --format '{{.Names}}' | grep -q "^\({CONTAINER_NAME}\)"; then
                            echo "   📦 Container ${CONTAINER_NAME} found, removing..."
                            
                            # Stop the container first (ignore errors if already stopped)
                            echo "   🛑 Stopping container..."
                            sudo docker stop ${CONTAINER_NAME} 2&gt;/dev/null || echo "   ℹ️ Container might already be stopped"
                            
                            # Wait a moment
                            sleep 2
                            
                            # Force remove the container
                            echo "   🗑️ Removing container..."
                            sudo docker rm -f ${CONTAINER_NAME} 2&gt;/dev/null || echo "   ⚠️ Failed to remove container"
                            
                            # Wait a moment for cleanup
                            sleep 2
                            
                        else
                            echo "   ℹ️ No container named ${CONTAINER_NAME} found"
                        fi
                        
                        # Method 2: Kill any process using the port
                        echo "2. Checking port ${APP_PORT} usage..."
                        
                        # Find process using the port
                        PORT_PID=\((sudo netstat -tlnp 2&gt;/dev/null | grep ":\){APP_PORT} " | awk '{print $7}' | cut -d'/' -f1 | head -1)
                        
                        if [ -n "\(PORT_PID" ] &amp;&amp; [ "\)PORT_PID" != "-" ] &amp;&amp; [ "$PORT_PID" != "" ]; then
                            echo "   ⚠️ Found process \(PORT_PID using port \){APP_PORT}"
                            echo "   💀 Killing process..."
                            sudo kill -9 "$PORT_PID" 2&gt;/dev/null || echo "   ℹ️ Process might already be dead"
                            sleep 2
                        else
                            echo "   ✅ Port ${APP_PORT} appears to be free"
                        fi
                        
                        # Method 3: Final verification and cleanup
                        echo "3. Final verification..."
                        
                        # Double-check container is gone
                        if docker ps -a --format '{{.Names}}' | grep -q "^\({CONTAINER_NAME}\)"; then
                            echo "   ⚠️ Container still exists, attempting nuclear removal..."
                            
                            # Get container ID
                            CONTAINER_ID=\((docker ps -a --format '{{.ID}}' --filter name=\){CONTAINER_NAME})
                            if [ -n "$CONTAINER_ID" ]; then
                                echo "   🎯 Container ID: $CONTAINER_ID"
                                
                                # Try to get and kill the main process
                                MAIN_PID=\((sudo docker inspect "\)CONTAINER_ID" 2&gt;/dev/null | grep '"Pid"' | head -1 | awk -F': ' '{print $2}' | tr -d ',' | tr -d ' ' 2&gt;/dev/null || echo "")
                                
                                if [ -n "\(MAIN_PID" ] &amp;&amp; [ "\)MAIN_PID" != "0" ] &amp;&amp; [ "$MAIN_PID" != "null" ]; then
                                    echo "   💀 Killing main process PID: $MAIN_PID"
                                    sudo kill -9 "$MAIN_PID" 2&gt;/dev/null || echo "   ℹ️ PID might already be dead"
                                    sleep 2
                                fi
                                
                                # Force remove with container ID
                                echo "   🗑️ Force removing by ID: $CONTAINER_ID"
                                sudo docker rm -f "$CONTAINER_ID" 2&gt;/dev/null || echo "   ⚠️ Failed to remove by ID"
                                sleep 2
                            fi
                        fi
                        
                        # Method 4: Clean up any orphaned containers
                        echo "4. Cleaning up orphaned containers..."
                        sudo docker container prune -f 2&gt;/dev/null || echo "   ℹ️ Container prune completed"
                        
                        # Final wait
                        sleep 3
                        
                        # Verification
                        echo "\\n=== Cleanup Verification ==="
                        if docker ps -a --format '{{.Names}}' | grep -q "^\({CONTAINER_NAME}\)"; then
                            echo "   ❌ ERROR: Container ${CONTAINER_NAME} still exists!"
                            echo "   📋 Existing containers:"
                            docker ps -a --format "table {{.Names}}\\t{{.Image}}\\t{{.Status}}" | grep ${CONTAINER_NAME} || true
                            echo "   🚨 This will cause deployment to fail!"
                        else
                            echo "   ✅ Container ${CONTAINER_NAME} successfully removed"
                        fi
                        
                        # Check port
                        if sudo netstat -tlnp 2&gt;/dev/null | grep -q ":${APP_PORT} "; then
                            echo "   ⚠️ WARNING: Port ${APP_PORT} still in use"
                            sudo netstat -tlnp 2&gt;/dev/null | grep ":${APP_PORT} " || true
                        else
                            echo "   ✅ Port ${APP_PORT} is available"
                        fi
                        
                        echo "\\n🎉 Cleanup process completed!"
                        
                        # Always exit with success to continue pipeline
                        exit 0
                    '''
                }
            }
        }
        
        stage('Deploy'){
            steps {
                script {
                    echo "🚀 Deploying application..."
                    
                    // Additional safety check before deployment
                    sh '''
                        echo "=== Pre-deployment Safety Check ==="
                        
                        # Final check if container still exists
                        if docker ps -a --format '{{.Names}}' | grep -q "^\({CONTAINER_NAME}\)"; then
                            echo "❌ FATAL: Container ${CONTAINER_NAME} still exists!"
                            echo "Attempting emergency removal..."
                            
                            # Emergency removal
                            docker stop ${CONTAINER_NAME} 2&gt;/dev/null || true
                            sleep 2
                            docker rm -f ${CONTAINER_NAME} 2&gt;/dev/null || true
                            sleep 2
                            
                            # Check again
                            if docker ps -a --format '{{.Names}}' | grep -q "^\({CONTAINER_NAME}\)"; then
                                echo "❌ CRITICAL: Unable to remove existing container!"
                                echo "Manual intervention required."
                                exit 1
                            fi
                        fi
                        
                        echo "✅ Safety check passed - ready for deployment"
                    '''
                    
                    // Wait a moment after cleanup
                    sleep 2
                    
                    // Deploy with correct port mapping
                    sh """
                        echo "Starting new container: ${CONTAINER_NAME}"
                        docker run -d \\
                            --name ${CONTAINER_NAME} \\
                            --restart unless-stopped \\
                            -p \({APP_PORT}:\){APP_PORT} \\
                            \({DOCKER_IMAGE}:\){DOCKER_TAG}
                        
                        echo "✅ Container started successfully"
                    """
                    
                    // Verify container is running
                    sh """
                        echo "=== Deployment Verification ==="
                        docker ps --format "table {{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}" | grep ${CONTAINER_NAME} || {
                            echo "❌ Container not found in running state!"
                            docker ps -a | grep ${CONTAINER_NAME} || echo "Container not found at all!"
                            exit 1
                        }
                        
                        echo "Container logs (first 20 lines):"
                        docker logs --tail 20 ${CONTAINER_NAME}
                    """
                }
            }
        }
        
        stage('Health Check') {
            steps {
                script {
                    echo "🏥 Performing health check..."
                    
                    sh """
                        echo "Waiting for application to start..."
                        sleep 10
                        
                        echo "Health check attempts:"
                        SUCCESS=false
                        
                        for i in 1 2 3 4 5; do
                            echo "Attempt \$i/5..."
                            
                            # Check if container is still running
                            if ! docker ps | grep -q ${CONTAINER_NAME}; then
                                echo "❌ Container ${CONTAINER_NAME} is not running!"
                                docker logs ${CONTAINER_NAME}
                                exit 1
                            fi
                            
                            # Check HTTP endpoint
                            if curl -f -s --max-time 10 http://localhost:${APP_PORT} &gt; /dev/null; then
                                echo "✅ Health check passed on attempt \$i!"
                                SUCCESS=true
                                break
                            else
                                echo "❌ Health check failed on attempt \$i"
                                if [ \$i -lt 5 ]; then
                                    echo "Waiting 10 seconds before retry..."
                                    sleep 10
                                fi
                            fi
                        done
                        
                        if [ "\$SUCCESS" = "false" ]; then
                            echo "❌ All health checks failed!"
                            echo "=== Debug Information ==="
                            docker ps | grep ${CONTAINER_NAME} || echo "Container not running"
                            docker logs --tail 50 ${CONTAINER_NAME}
                            exit 1
                        fi
                        
                        echo "🎉 Application is healthy and running!"
                    """
                }
            }
        }
    }
    
    post {
        always {
            // Clean up Docker images to save space (but keep recent ones)
            sh 'docker image prune -f --filter "until=24h"'
        }
        success {
            echo '✅ Pipeline completed successfully!'
            echo "🚀 Application is accessible at: http://100.82.148.95:${APP_PORT}"
            
            // Show deployment summary
            sh """
                echo "=== Deployment Summary ==="
                echo "Image: \({DOCKER_IMAGE}:\){DOCKER_TAG}"
                echo "Container: ${CONTAINER_NAME}"
                echo "Port: ${APP_PORT}"
                echo "Build: ${BUILD_NUMBER}"
                echo "Time: \$(date)"
                echo "Status: \\((docker inspect --format='{{.State.Status}}' \){CONTAINER_NAME})"
                echo "=========================="
            """
        }
        failure {
            echo '❌ Pipeline failed!'
            // Enhanced debugging information
            sh '''
                echo "=== Failure Debug Information ==="
                echo "Docker containers (all):"
                docker ps -a --format "table {{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}" || true
                
                echo "\\nPort usage:"
                netstat -tlnp 2&gt;/dev/null | grep ":5000 " || echo "No process using port 5000"
                
                echo "\\nContainer logs (if exists):"
                docker logs --tail 50 ${CONTAINER_NAME} 2&gt;/dev/null || echo "No container logs available"
                
                echo "\\nDocker system info:"
                docker system df || true
                echo "========================"
            '''
        }
        cleanup {
            // Clean workspace
            cleanWs()
        }
    }
}
</code></pre>
<p>Then, click the <strong>"Build Now"</strong> button. This will start the pipeline build process and display the build status.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750959004408/e424242f-4c45-406d-ad75-6f5a73be215b.png" alt="" />

<h2>Step 6: Setting Up GitHub Web hook with Jenkins</h2>
<hr />
<h4>Prerequisites</h4>
<ul>
<li><p>Jenkins must be publicly accessible (not behind a private IP or Tailscale-only address).</p>
</li>
<li><p>Jenkins pipeline is already configured with your GitHub repo.</p>
</li>
<li><p>Webhook endpoint URL (replace with your actual IP or domain):</p>
</li>
</ul>
<pre><code class="language-plaintext">http://&lt;server-ip&gt;:8080/github-webhook/
</code></pre>
<blockquote>
<p>💡 If you're using <strong>Tailscale</strong>, expose Jenkins using <strong>Tailscale Funnel</strong>:</p>
</blockquote>
<pre><code class="language-bash">sudo tailscale funnel 8080
</code></pre>
<p>This will give a public URL like:</p>
<pre><code class="language-plaintext">https://yourname.ts.net/github-webhook/
</code></pre>
<h3>🪝 Steps to Add GitHub Web hook</h3>
<ol>
<li><p>Go to your GitHub repository.</p>
</li>
<li><p>Navigate to: <strong>Settings</strong> → <strong>Web-hooks</strong> → <strong>Add web-hook</strong></p>
</li>
<li><p>In the <strong>Payload URL</strong>, enter:</p>
<pre><code class="language-plaintext">http://&lt;your-server-ip&gt;:8080/github-webhook/
</code></pre>
<p><em>(Use your Tail-scale Funnel URL if applicable)</em></p>
</li>
<li><p>Set <strong>Content type</strong> to:</p>
<pre><code class="language-plaintext">application/json
</code></pre>
</li>
<li><p>Choose:</p>
<ul>
<li><strong>Just the push event</strong></li>
</ul>
</li>
<li><p>Click <strong>Add web hook</strong></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750959068749/fcce35c5-c332-4abe-af2a-4ff85714ebc2.png" alt="" />

<h2>Step 7: Update Repository with New Application – SEO Content Optimizer Tool (Gen AI App)</h2>
<p>After verifying the Jenkins pipeline works with your basic Node.js app, we now replace it with the actual <strong>Gen AI application</strong> – a <strong>SEO Content Optimizer Tool</strong>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750959109865/60079398-d485-461a-a735-ecdb00c076f5.jpeg" alt="" />

<h3>🚀 What's Being Deployed?</h3>
<p>This new version of the app uses <strong>Generative AI</strong> techniques to analyze and optimize content for better SEO performance. It provides:</p>
<ul>
<li><p>Keyword extraction</p>
</li>
<li><p>Meta tag suggestions</p>
</li>
<li><p>Readability analysis</p>
</li>
<li><p>Title and description optimization</p>
</li>
<li><p>AI-based content suggestions</p>
</li>
</ul>
<hr />
<h3>📦 Steps to Update the Application Code</h3>
<ol>
<li><p><strong>Replace Code Locally</strong><br />Update your project folder (<code>my-node-app/</code>) with the files for the SEO Optimizer Tool.</p>
<blockquote>
<p>Make sure it includes your <code>Dockerfile</code>, updated <code>index.js</code>, and any other required modules like <code>openai</code>, <code>axios</code>, etc.</p>
</blockquote>
</li>
<li><p><strong>Install New Dependencies</strong> (if applicable)</p>
</li>
</ol>
<pre><code class="language-bash">   npm install
</code></pre>
<hr />
<h2>❗Common Errors &amp; Fixes</h2>
<p>Below is a list of the key issues encountered during the CI/CD setup and deployment of the SEO Content Optimizer tool, along with the solutions applied.</p>
<h3>🔐 1. Jenkins Initial Admin Password Not Found</h3>
<p><strong>Problem:</strong><br />Jenkins prompts for an initial admin password but the default command didn’t work.</p>
<p><strong>Default command:</strong></p>
<pre><code class="language-bash">sudo cat /var/jenkins_home/secrets/initialAdminPassword
</code></pre>
<p><strong>✅ Fix:</strong> If Jenkins runs as a Docker container, use:</p>
<pre><code class="language-bash">docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
</code></pre>
<hr />
<h3>☕ 2. JDK Path Not Found for Jenkins Build Tools</h3>
<p><strong>Problem:</strong><br />Unable to configure JDK path under Jenkins build tool settings.</p>
<p><strong>✅ Fix:</strong></p>
<pre><code class="language-bash">sudo apt update
sudo apt install openjdk-17-jdk -y
</code></pre>
<p>Set the path in Jenkins as:</p>
<pre><code class="language-plaintext">/usr/lib/jvm/java-17-openjdk-amd64
</code></pre>
<hr />
<h3>📦 3. Node.js App Not Starting</h3>
<p><strong>Problem:</strong><br />Server failed to start with this error:</p>
<blockquote>
<p><code>Error [ERR_REQUIRE_ESM]: Must use import to load ES Module</code></p>
</blockquote>
<p><strong>✅ Fix:</strong><br />Add the following to your <code>package.json</code>:</p>
<pre><code class="language-json">"type": "module",
</code></pre>
<p>Then re-run:</p>
<pre><code class="language-bash">node index.js
</code></pre>
<hr />
<h3>🔐 4. GitHub Push Authentication Failed</h3>
<p><strong>Problem:</strong><br />While pushing code to GitHub, password authentication failed.</p>
<p><strong>✅ Fix:</strong><br />Use a <strong>Personal Access Token (PAT)</strong> instead of your GitHub account password.</p>
<p><strong>Steps to create a token:</strong></p>
<ul>
<li><p>Go to <a href="https://github.com/settings/tokens">GitHub Token Settings</a></p>
</li>
<li><p>Click <strong>Generate new token (classic)</strong></p>
</li>
<li><p>Select required scopes (repo access)</p>
</li>
<li><p>Copy the token and use it as password in the terminal</p>
</li>
</ul>
<hr />
<h3>🐳 5. Docker Commands Failed in Jenkins Pipeline</h3>
<p><strong>Problem:</strong><br />Jenkins pipeline fails during Docker stages with messages like:</p>
<blockquote>
<p><code>docker: command not found</code><br />or<br /><code>permission denied</code></p>
</blockquote>
<p><strong>Root Cause:</strong><br />Jenkins was using the <strong>Snap Docker</strong> version while the system used <strong>APT Docker</strong>, causing conflicts. Jenkins had no permission to access the host Docker daemon.</p>
<p><strong>✅ Fix:</strong></p>
<ol>
<li><p>Uninstall both versions:</p>
<pre><code class="language-bash">sudo snap remove docker
sudo apt remove docker docker.io
</code></pre>
</li>
<li><p>Install Docker cleanly via APT:</p>
<pre><code class="language-bash">sudo apt install docker.io -y
</code></pre>
</li>
<li><p>Add Jenkins to Docker group:</p>
<pre><code class="language-bash">sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
</code></pre>
</li>
<li><p>Verify access:</p>
<pre><code class="language-bash">sudo -u jenkins docker ps
</code></pre>
</li>
</ol>
<hr />
<h3>🔄 6. Jenkins Container Doesn’t Persist After Reboot</h3>
<p><strong>Problem:</strong><br />After rebooting the server, the app is no longer accessible.<br />No container is running.</p>
<p><strong>✅ Fix Options:</strong></p>
<ul>
<li>Use the <code>--restart unless-stopped</code> flag when starting containers in Jenkins:</li>
</ul>
<pre><code class="language-bash">docker run -d --name node-app-container --restart unless-stopped -p 5000:5000 &lt;image-name&gt;
</code></pre>
<ul>
<li>Ensure Docker starts on boot:</li>
</ul>
<pre><code class="language-bash">sudo systemctl enable docker
</code></pre>
<ul>
<li>Optionally add a health check stage to re-deploy if the container isn't running.</li>
</ul>
]]></content:encoded></item></channel></rss>