Index: .github/workflows/ci-cd.yml
===================================================================
--- .github/workflows/ci-cd.yml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
+++ .github/workflows/ci-cd.yml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
@@ -0,0 +1,151 @@
+name: CI/CD
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches: [master]
+
+env:
+  BACKEND_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/trekr-backend
+  FRONTEND_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/trekr-frontend
+
+jobs:
+  # ── CI: build both images on every push / PR ──────────────────────────────
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Build backend image (no push)
+        uses: docker/build-push-action@v5
+        with:
+          context: ./backend
+          push: false
+          cache-from: type=gha,scope=backend
+          cache-to:   type=gha,scope=backend,mode=max
+
+      - name: Build frontend image (no push)
+        uses: docker/build-push-action@v5
+        with:
+          context: ./frontend
+          push: false
+          cache-from: type=gha,scope=frontend
+          cache-to:   type=gha,scope=frontend,mode=max
+
+  # ── CD part 1: push images to DockerHub (master only) ─────────────────────
+  push:
+    name: Push to DockerHub
+    runs-on: ubuntu-latest
+    needs: build
+    if: github.ref == 'refs/heads/master' && github.event_name == 'push'
+    outputs:
+      image_tag: ${{ github.sha }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Log in to DockerHub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Build and push backend
+        uses: docker/build-push-action@v5
+        with:
+          context: ./backend
+          push: true
+          tags: |
+            ${{ env.BACKEND_IMAGE }}:latest
+            ${{ env.BACKEND_IMAGE }}:${{ github.sha }}
+          cache-from: type=gha,scope=backend
+          cache-to:   type=gha,scope=backend,mode=max
+
+      - name: Build and push frontend
+        uses: docker/build-push-action@v5
+        with:
+          context: ./frontend
+          push: true
+          tags: |
+            ${{ env.FRONTEND_IMAGE }}:latest
+            ${{ env.FRONTEND_IMAGE }}:${{ github.sha }}
+          cache-from: type=gha,scope=frontend
+          cache-to:   type=gha,scope=frontend,mode=max
+
+  # ── CD part 2: deploy to k3s cluster ──────────────────────────────────────
+  deploy:
+    name: Deploy to Kubernetes
+    runs-on: ubuntu-latest
+    needs: push
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up kubectl
+        uses: azure/setup-kubectl@v3
+        with:
+          version: latest
+
+      - name: Configure kubeconfig
+        run: |
+          echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > $HOME/kubeconfig.yaml
+          chmod 600 $HOME/kubeconfig.yaml
+          echo "KUBECONFIG=$HOME/kubeconfig.yaml" >> $GITHUB_ENV
+
+      - name: Apply namespace
+        run: kubectl apply -f k8s/namespace.yaml
+
+      - name: Create / update app secrets
+        run: |
+          kubectl create secret generic trekr-secrets \
+            --namespace=trekr \
+            --from-literal=db-password=${{ secrets.DB_PASSWORD }} \
+            --from-literal=jwt-secret=${{ secrets.JWT_SECRET }} \
+            --save-config \
+            --dry-run=client -o yaml | kubectl apply -f -
+
+      - name: Create DB init ConfigMap from DDL
+        run: |
+          kubectl create configmap trekr-db-init \
+            --namespace=trekr \
+            --from-file=01-ddl.sql=db-scripts/ddl.sql \
+            --save-config \
+            --dry-run=client -o yaml | kubectl apply -f -
+
+      - name: Apply Postgres manifests
+        run: kubectl apply -f k8s/postgres.yaml
+
+      - name: Wait for Postgres to be ready
+        run: kubectl rollout status statefulset/postgres -n trekr --timeout=120s
+
+      - name: Apply backend manifests
+        run: kubectl apply -f k8s/backend.yaml
+
+      - name: Apply frontend manifests
+        run: kubectl apply -f k8s/frontend.yaml
+
+      - name: Roll out new images
+        run: |
+          kubectl set image deployment/backend \
+            backend=${{ env.BACKEND_IMAGE }}:${{ github.sha }} \
+            -n trekr
+          kubectl set image deployment/frontend \
+            frontend=${{ env.FRONTEND_IMAGE }}:${{ github.sha }} \
+            -n trekr
+
+      - name: Wait for rollout to complete
+        run: |
+          kubectl rollout status deployment/backend  -n trekr --timeout=180s
+          kubectl rollout status deployment/frontend -n trekr --timeout=60s
+
+      - name: Print app URL
+        run: |
+          NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}' 2>/dev/null || \
+                    kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')
+          echo "App available at: http://${NODE_IP}:30080"
Index: backend/src/main/java/com/trekr/backend/controller/AuthController.java
===================================================================
--- backend/src/main/java/com/trekr/backend/controller/AuthController.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ backend/src/main/java/com/trekr/backend/controller/AuthController.java	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
@@ -31,3 +31,8 @@
         return ResponseEntity.ok(response);
     }
+
+    @GetMapping("/ping")
+    public ResponseEntity<Void> ping() {
+        return ResponseEntity.ok().build();
+    }
 }
Index: k8s/backend.yaml
===================================================================
--- k8s/backend.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
+++ k8s/backend.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
@@ -0,0 +1,66 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend
+  namespace: trekr
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backend
+  template:
+    metadata:
+      labels:
+        app: backend
+    spec:
+      initContainers:
+        - name: wait-for-postgres
+          image: postgres:16-alpine
+          command: [sh, -c, until pg_isready -h postgres -p 5432 -U trekr; do sleep 2; done]
+      containers:
+        - name: backend
+          image: trekr-backend:latest  # overwritten by pipeline via kubectl set image
+          env:
+            - name: SPRING_DATASOURCE_URL
+              value: jdbc:postgresql://postgres:5432/trekr
+            - name: SPRING_DATASOURCE_USERNAME
+              value: trekr
+            - name: SPRING_DATASOURCE_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: trekr-secrets
+                  key: db-password
+            - name: SPRING_JPA_HIBERNATE_DDL_AUTO
+              value: none
+            - name: JWT_SECRET
+              valueFrom:
+                secretKeyRef:
+                  name: trekr-secrets
+                  key: jwt-secret
+          ports:
+            - containerPort: 8080
+          readinessProbe:
+            httpGet:
+              path: /api/auth/ping
+              port: 8080
+            initialDelaySeconds: 20
+            periodSeconds: 10
+            failureThreshold: 6
+          livenessProbe:
+            httpGet:
+              path: /api/auth/ping
+              port: 8080
+            initialDelaySeconds: 60
+            periodSeconds: 15
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: backend
+  namespace: trekr
+spec:
+  selector:
+    app: backend
+  ports:
+    - port: 8080
+      targetPort: 8080
Index: k8s/frontend.yaml
===================================================================
--- k8s/frontend.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
+++ k8s/frontend.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
@@ -0,0 +1,40 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: frontend
+  namespace: trekr
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: frontend
+  template:
+    metadata:
+      labels:
+        app: frontend
+    spec:
+      containers:
+        - name: frontend
+          image: trekr-frontend:latest  # overwritten by pipeline via kubectl set image
+          ports:
+            - containerPort: 80
+          readinessProbe:
+            httpGet:
+              path: /
+              port: 80
+            initialDelaySeconds: 5
+            periodSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: frontend
+  namespace: trekr
+spec:
+  type: NodePort
+  selector:
+    app: frontend
+  ports:
+    - port: 80
+      targetPort: 80
+      nodePort: 30080
Index: k8s/namespace.yaml
===================================================================
--- k8s/namespace.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
+++ k8s/namespace.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: trekr
Index: k8s/postgres.yaml
===================================================================
--- k8s/postgres.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
+++ k8s/postgres.yaml	(revision c0086c00a5899d38577d47ee07ef2f1dc672069e)
@@ -0,0 +1,71 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: postgres-pvc
+  namespace: trekr
+spec:
+  accessModes: [ReadWriteOnce]
+  resources:
+    requests:
+      storage: 5Gi
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: postgres
+  namespace: trekr
+spec:
+  serviceName: postgres
+  replicas: 1
+  selector:
+    matchLabels:
+      app: postgres
+  template:
+    metadata:
+      labels:
+        app: postgres
+    spec:
+      containers:
+        - name: postgres
+          image: postgres:16-alpine
+          env:
+            - name: POSTGRES_DB
+              value: trekr
+            - name: POSTGRES_USER
+              value: trekr
+            - name: POSTGRES_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: trekr-secrets
+                  key: db-password
+          ports:
+            - containerPort: 5432
+          volumeMounts:
+            - name: data
+              mountPath: /var/lib/postgresql/data
+            - name: init-sql
+              mountPath: /docker-entrypoint-initdb.d
+          readinessProbe:
+            exec:
+              command: [pg_isready, -U, trekr, -d, trekr]
+            initialDelaySeconds: 5
+            periodSeconds: 5
+      volumes:
+        - name: data
+          persistentVolumeClaim:
+            claimName: postgres-pvc
+        - name: init-sql
+          configMap:
+            name: trekr-db-init
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: postgres
+  namespace: trekr
+spec:
+  selector:
+    app: postgres
+  ports:
+    - port: 5432
+      targetPort: 5432
