32 KiB
On-Premise Kubernetes Deployment Guide
React + Node.js + MySQL — Production Deployment
Table of Contents
- Prerequisites
- Project Structure
- Pre-Deployment Checklist
- Step-by-Step Deployment
- Verify the Deployment
- Troubleshooting Guide
- Updating the Application
- Teardown
- CI/CD — Jenkins Multibranch Pipeline
- GitOps — ArgoCD (Reference)
1. Prerequisites
Required Tools (on your workstation)
| Tool | Minimum Version | Check Command |
|---|---|---|
| kubectl | v1.24+ | kubectl version --client |
| kustomize | v5.0+ (or use kubectl built-in) | kubectl kustomize --help |
| docker | v20+ | docker --version |
| helm | v3.0+ | helm version |
Required on the Kubernetes Cluster
| Component | Purpose | Install Command |
|---|---|---|
| NGINX Ingress Controller | Routes external traffic | See step 4.1 |
| local-path Provisioner | Provides PersistentVolumes | See step 4.2 |
myapp.local DNS or hosts entry |
Browser access | See step 4.6 |
Cluster Requirements
- Kubernetes v1.24+
- At least 1 worker node with:
- 1 CPU core free
- 2 GB RAM free
- 5 GB disk free (2 GB for MySQL PVC + OS overhead)
kubectlconfigured with cluster access (kubectl get nodesreturns Ready)
2. Project Structure
k8s/
├── base/ # Shared manifests (all environments)
│ ├── kustomization.yaml # Generates ConfigMap + Secret from property files
│ ├── config.properties # Non-sensitive env vars (DB_HOST, DB_PORT, etc.)
│ ├── secret.properties # Sensitive env vars (passwords) — gitignored
│ ├── namespace.yaml # Namespace: react-mysql
│ ├── mysql/
│ │ ├── configmap-sql.yaml # Init SQL mounted at /docker-entrypoint-initdb.d/
│ │ ├── statefulset.yaml # MySQL 8.0 StatefulSet with PVC + probes
│ │ └── service.yaml # Headless ClusterIP service for StatefulSet DNS
│ ├── backend/
│ │ ├── deployment.yaml # Node.js API with init container (waits for MySQL)
│ │ └── service.yaml # ClusterIP :3000
│ └── frontend/
│ ├── deployment.yaml # React static site served by `serve`
│ └── service.yaml # ClusterIP :3000
└── overlays/
└── onpremise/
├── kustomization.yaml # Extends base, adds ingress
├── ingress.yaml # Two Ingress objects (API rewrite + frontend)
└── patch-storageclass.yaml # Patches MySQL PVC → storageClassName: local-path
How Traffic Flows
Browser
│
▼
[myapp.local:80]
│
▼
NGINX Ingress Controller
├── /api/* ──rewrite /api→/──► backend Service :3000 ──► Node.js Pod
│ │
│ ▼
│ MySQL Service
│ │
│ ▼
│ mysql-0 Pod
│ │
│ ▼
│ PVC (local-path 2Gi)
│
└── / ──────────────────────► frontend Service :3000 ──► React Pod
3. Pre-Deployment Checklist
Run through this before deploying:
# 1. Confirm cluster is reachable
kubectl get nodes
# 2. Confirm you have cluster-admin rights
kubectl auth can-i create namespace --all-namespaces
# 3. Confirm Docker Hub images exist
docker manifest inspect subkamble/react-mysql-backend:latest
docker manifest inspect subkamble/react-mysql-frontend:latest
# 4. Confirm secret.properties exists (it's gitignored, must be created manually)
cat k8s/base/secret.properties
Expected output for step 4:
DB_PASSWORD=pass123
MYSQL_ROOT_PASSWORD=pass123
MYSQL_PASSWORD=pass123
If secret.properties is missing, create it:
cat > k8s/base/secret.properties <<EOF DB_PASSWORD=pass123 MYSQL_ROOT_PASSWORD=pass123 MYSQL_PASSWORD=pass123 EOFReplace
pass123with a strong password in production.
4. Step-by-Step Deployment
Step 4.1 — Install NGINX Ingress Controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.replicaCount=1
# Wait for it to be ready (takes 1-2 min)
kubectl wait deployment/ingress-nginx-controller \
-n ingress-nginx \
--for=condition=Available \
--timeout=120s
# Verify
kubectl get pods -n ingress-nginx
Expected output:
NAME READY STATUS RESTARTS
ingress-nginx-controller-xxxxxxxxx-xxxxx 1/1 Running 0
Step 4.2 — Install local-path Storage Provisioner
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
# Wait for it to be ready
kubectl wait deployment/local-path-provisioner \
-n local-path-storage \
--for=condition=Available \
--timeout=60s
# Verify the StorageClass exists
kubectl get storageclass
Expected output includes:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
local-path rancher.io/local-path Delete WaitForFirstConsumer
Step 4.3 — Verify Kustomize Output (Dry Run)
Always preview what will be applied before deploying:
kubectl kustomize k8s/overlays/onpremise
Verify the output includes:
storageClassName: local-pathin the StatefulSet volumeClaimTemplate- Both Ingress objects (
react-mysql-api-ingressandreact-mysql-frontend-ingress) - ConfigMap
app-config-<hash>and Secretapp-secret-<hash>
Step 4.4 — Deploy
kubectl apply -k k8s/overlays/onpremise
Expected output:
namespace/react-mysql created
configmap/app-config-7fm29c526g created
configmap/mysql-init-sql created
secret/app-secret-7ht4dgtbc6 created
service/backend created
service/frontend created
service/mysql created
deployment.apps/backend created
deployment.apps/frontend created
statefulset.apps/mysql created
ingress.networking.k8s.io/react-mysql-api-ingress created
ingress.networking.k8s.io/react-mysql-frontend-ingress created
Step 4.5 — Watch Pods Come Up
kubectl get pods -n react-mysql -w
Expected sequence:
| Pod | First status | Final status | Time |
|---|---|---|---|
mysql-0 |
ContainerCreating |
1/1 Running |
~30s |
backend-xxx |
Init:0/1 (waiting for MySQL) |
1/1 Running |
~60s |
frontend-xxx |
Running |
1/1 Running |
~20s |
Note:
backendstaying inInit:0/1is normal — the init container runsnc -z mysql 3306in a loop until MySQL's readiness probe passes. This prevents backend crash-loops due to MySQL not being ready.
Step 4.6 — Configure DNS / hosts
Get the external IP of the Ingress controller:
kubectl get svc -n ingress-nginx ingress-nginx-controller
Look for the EXTERNAL-IP column. Then:
Option A — /etc/hosts (single machine, testing):
echo "<EXTERNAL-IP> myapp.local" | sudo tee -a /etc/hosts
Option B — Internal DNS (production): Create an A record in your internal DNS server:
myapp.local → <EXTERNAL-IP>
Step 4.7 — Verify Ingress Has an Address
kubectl get ingress -n react-mysql
Expected output:
NAME CLASS HOSTS ADDRESS PORTS
react-mysql-api-ingress nginx myapp.local <EXTERNAL-IP> 80
react-mysql-frontend-ingress nginx myapp.local <EXTERNAL-IP> 80
If
ADDRESSis empty after 2 minutes, see Ingress has no ADDRESS.
5. Verify the Deployment
Run these checks in order:
# 1. All pods Running
kubectl get pods -n react-mysql
# 2. Database table exists
kubectl exec mysql-0 -n react-mysql -- \
mysql -u root -ppass123 -e "SHOW TABLES FROM appdb;"
# 3. Backend API responds
kubectl port-forward svc/backend 3000:3000 -n react-mysql &
curl localhost:3000/user
# Expected: []
kill %1
# 4. Ingress routes API correctly
curl http://myapp.local/api/user
# Expected: []
# 5. Post a user
curl -X POST http://myapp.local/api/user \
-H "Content-Type: application/json" \
-d '{"data":"Alice"}'
# Expected: {"affectedRows":1,...}
# 6. User persists
curl http://myapp.local/api/user
# Expected: [{"id":1,"name":"Alice"}]
# 7. Frontend loads (check Content-Type is text/html)
curl -si http://myapp.local/ | head -5
# 8. Static JS asset loads (must be application/javascript, NOT text/html)
curl -si http://myapp.local/static/js/main.ff70bc14.js | head -3
# 9. PVC persistence — delete MySQL pod and verify data survives
kubectl delete pod mysql-0 -n react-mysql
kubectl wait pod/mysql-0 -n react-mysql --for=condition=Ready --timeout=90s
sleep 10
curl http://myapp.local/api/user
# Expected: [{"id":1,"name":"Alice"}] ← Alice survived pod restart
Open http://myapp.local in a browser — submit a name, it should appear in the table.
6. Troubleshooting Guide
ERROR: ImagePullBackOff or ErrImagePull
Symptom:
NAME READY STATUS RESTARTS
backend-xxx 0/1 ImagePullBackOff 0
Diagnose:
kubectl describe pod -n react-mysql -l app=backend | grep -A10 "Events:"
Cause A — Image does not exist on Docker Hub:
Failed to pull image: pull access denied, repository does not exist
Fix: Build and push the images.
docker build -t subkamble/react-mysql-backend:latest ./backend
docker push subkamble/react-mysql-backend:latest
docker build -t subkamble/react-mysql-frontend:latest ./frontend
docker push subkamble/react-mysql-frontend:latest
Cause B — Docker Hub rate limit:
toomanyrequests: You have reached your pull rate limit
Fix: Pre-load images onto each node, then set imagePullPolicy: Never.
# On each cluster node (SSH in):
docker pull subkamble/react-mysql-backend:latest
docker pull subkamble/react-mysql-frontend:latest
Or use a private registry and update image references in the deployments.
Cause C — Private registry, no credentials:
Create an imagePullSecret:
kubectl create secret docker-registry regcred \
--docker-server=docker.io \
--docker-username=<username> \
--docker-password=<token> \
-n react-mysql
Then add to both deployments:
spec:
imagePullSecrets:
- name: regcred
ERROR: mysql-0 stays Pending
Symptom:
NAME READY STATUS RESTARTS
mysql-0 0/1 Pending 0
Diagnose:
kubectl describe pod mysql-0 -n react-mysql | grep -A10 "Events:"
kubectl get pvc -n react-mysql
Cause A — No StorageClass named local-path:
0/1 nodes are available: pod has unbound immediate PersistentVolumeClaims
Fix: Install the local-path provisioner (Step 4.2).
kubectl get storageclass # local-path must be present
Cause B — WaitForFirstConsumer binding (normal behavior):
The local-path StorageClass uses WaitForFirstConsumer — the PVC stays Pending
until the pod is scheduled. This resolves automatically. Wait 30 seconds and check again.
Cause C — Node has insufficient disk space:
Insufficient disk space on node
Fix: Free up disk on the node or add a new node with enough space.
# Check node disk usage
kubectl describe node <node-name> | grep -A5 "Allocatable"
ERROR: Frontend crash-loops (CrashLoopBackOff)
Symptom:
NAME READY STATUS RESTARTS
frontend-xxx 0/1 CrashLoopBackOff 4
Diagnose:
kubectl logs -n react-mysql -l app=frontend --previous
Cause A — Old image cached on node (stale npx serve):
npm warn exec The following package was not found and will be installed: serve@14.2.6
npm error signal SIGTERM
The node has the old Docker image (CMD: npx serve) instead of the fixed one
(CMD: serve). The liveness probe kills the container before npx finishes
downloading serve.
Fix:
# Check which CMD the image on the node has
# SSH into the affected node, then:
docker inspect subkamble/react-mysql-frontend:latest \
--format='{{json .Config.Cmd}}'
# If output is ["npx","serve","-s","build"] → stale image
# Force remove and re-pull
docker rmi -f subkamble/react-mysql-frontend:latest
docker pull subkamble/react-mysql-frontend:latest
# Verify fix — output must be ["serve","-s","build"]
docker inspect subkamble/react-mysql-frontend:latest \
--format='{{json .Config.Cmd}}'
# Then restart the pod
kubectl rollout restart deployment/frontend -n react-mysql
Cause B — Liveness probe firing too early: The container starts but port 3000 isn't bound yet when the probe fires.
Liveness probe failed: Get "http://10.x.x.x:3000/": connection refused
Fix: Increase initialDelaySeconds in k8s/base/frontend/deployment.yaml:
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 40 # increase from 20 → 40
periodSeconds: 20
Then re-apply:
kubectl apply -k k8s/overlays/onpremise
ERROR: Backend stays in Init:0/1
Symptom:
NAME READY STATUS RESTARTS
backend-xxx 0/1 Init:0/1 0
Diagnose:
kubectl logs -n react-mysql -l app=backend -c wait-for-mysql
Cause A — MySQL readiness probe hasn't passed yet (normal for ~30s):
Wait 60 seconds. The init container loops nc -z mysql 3306 until MySQL's
readiness probe (mysqladmin ping) passes.
Cause B — MySQL pod is not Running:
kubectl get pods -n react-mysql -l app=mysql
Fix the MySQL pod first (see MySQL errors below), then backend will unblock.
Cause C — MySQL service DNS not resolving:
# Test from another pod
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never \
-n react-mysql -- nslookup mysql
Expected: resolves to the MySQL pod IP. If it fails, check the headless service:
kubectl get svc mysql -n react-mysql
# clusterIP must be "None"
ERROR: MySQL pod fails (CrashLoopBackOff)
Diagnose:
kubectl logs mysql-0 -n react-mysql
Cause A — Wrong password in secret:
[ERROR] Access denied for user 'root'@'localhost'
Fix: Verify secret.properties has matching passwords and re-apply:
cat k8s/base/secret.properties
# DB_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD must all match
kubectl apply -k k8s/overlays/onpremise
Cause B — Corrupted PVC data (e.g. previous failed init):
[ERROR] InnoDB: Cannot open datafile for read-write
Fix: Delete the PVC to wipe and re-initialize:
kubectl delete statefulset mysql -n react-mysql
kubectl delete pvc mysql-data-mysql-0 -n react-mysql
kubectl apply -k k8s/overlays/onpremise
Warning: This deletes all database data.
ERROR: Ingress has no ADDRESS
Symptom:
NAME CLASS HOSTS ADDRESS PORTS
react-mysql-api-ingress nginx myapp.local 80
ADDRESS column is empty after 2+ minutes.
Diagnose:
kubectl get pods -n ingress-nginx
kubectl describe ingress react-mysql-api-ingress -n react-mysql
Cause A — NGINX Ingress Controller not installed:
kubectl get pods -n ingress-nginx
# No resources found
Fix: Install it (Step 4.1).
Cause B — Ingress controller pod not Ready:
kubectl get pods -n ingress-nginx
# STATUS = Pending or CrashLoopBackOff
Fix:
kubectl describe pod -n ingress-nginx -l app.kubernetes.io/component=controller \
| grep -A10 "Events:"
Common sub-cause: node port conflict. Check if port 80/443 is in use on the node.
Cause C — Wrong ingressClassName:
The Ingress specifies ingressClassName: nginx but the controller was installed
with a different class name.
kubectl get ingressclass
# NAME CONTROLLER
# nginx k8s.io/ingress-nginx ← must match
If the class name differs, patch the ingress YAML or reinstall the controller.
ERROR: White/blank screen in browser
Symptom: Page loads but shows only a blank white screen. No visible errors.
Diagnose in browser: Open DevTools → Console. You'll see:
Failed to load resource: the server responded with a status of 200 (OK)
/static/js/main.ff70bc14.js
The JS file is returning HTML instead of JavaScript.
Cause — Single Ingress with global rewrite-target:
If both frontend and API paths are in one Ingress object with
rewrite-target: /$2, the rewrite applies to all paths — static assets
(/static/js/...) get rewritten to /, returning index.html.
Verify:
curl -si http://myapp.local/static/js/main.ff70bc14.js | head -3
# BAD: Content-Type: text/html ← returning index.html
# GOOD: Content-Type: application/javascript
Fix: Ensure two separate Ingress objects are used — one for /api with
the rewrite annotation, one for / without:
kubectl get ingress -n react-mysql
# Must show TWO ingress objects:
# react-mysql-api-ingress ← has rewrite-target annotation
# react-mysql-frontend-ingress ← no rewrite annotation
If only one exists, re-apply the overlay:
kubectl delete ingress -n react-mysql --all
kubectl apply -k k8s/overlays/onpremise
ERROR: curl http://myapp.local/api/user returns 404
Diagnose:
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller | tail -20
Cause A — /api path not matching:
Test the rewrite directly:
curl -si http://myapp.local/api/user
# Check X-Original-URI header in nginx logs
Cause B — Backend service unreachable:
kubectl exec -n react-mysql -l app=backend -- \
wget -qO- localhost:3000/user
Cause C — Webhook admission error on Ingress creation:
Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io"
The NGINX ingress admission webhook wasn't ready when you applied. Fix:
# Wait for ingress controller to be fully ready first
kubectl wait deployment/ingress-nginx-controller \
-n ingress-nginx --for=condition=Available --timeout=120s
# Then re-apply
kubectl apply -k k8s/overlays/onpremise
ERROR: Data lost after MySQL pod restart
Symptom: After kubectl delete pod mysql-0, all rows are gone.
Diagnose:
kubectl get pvc -n react-mysql
# STATUS must be Bound, not Pending or Lost
Cause A — PVC is in Lost state:
The underlying PersistentVolume was deleted or the node was replaced.
kubectl describe pvc mysql-data-mysql-0 -n react-mysql
This is unrecoverable without a backup. Re-initialize:
kubectl delete pvc mysql-data-mysql-0 -n react-mysql
kubectl delete pod mysql-0 -n react-mysql
# StatefulSet recreates the pod + new PVC
Cause B — Wrong StorageClass (data not actually persisted):
kubectl get pvc mysql-data-mysql-0 -n react-mysql -o jsonpath='{.spec.storageClassName}'
# Must output: local-path
If it shows standard (minikube default), the on-prem patch wasn't applied.
Check the overlay is being used:
kubectl kustomize k8s/overlays/onpremise | grep storageClassName
# Must output: storageClassName: local-path
7. Updating the Application
Update backend or frontend code
# Rebuild image
docker build -t subkamble/react-mysql-backend:latest ./backend
docker push subkamble/react-mysql-backend:latest
# If nodes cache images locally, SSH into each node and force re-pull:
# docker rmi -f subkamble/react-mysql-backend:latest
# docker pull subkamble/react-mysql-backend:latest
# Rolling restart (zero downtime)
kubectl rollout restart deployment/backend -n react-mysql
# Watch rollout
kubectl rollout status deployment/backend -n react-mysql
Update ConfigMap values (non-sensitive config)
# Edit k8s/base/config.properties, then re-apply
kubectl apply -k k8s/overlays/onpremise
# Restart pods to pick up new ConfigMap
kubectl rollout restart deployment/backend deployment/frontend -n react-mysql
Update Secret values (passwords)
# Edit k8s/base/secret.properties, then re-apply
kubectl apply -k k8s/overlays/onpremise
kubectl rollout restart deployment/backend -n react-mysql
Note: Changing
MYSQL_ROOT_PASSWORDafter MySQL has initialized will NOT change the database password — MySQL stores it internally in the PVC. To change the DB password: exec intomysql-0and runALTER USER.
8. Teardown
Remove the application only (keep cluster intact)
kubectl delete namespace react-mysql
Warning: This deletes the PVC and all MySQL data permanently.
Remove NGINX Ingress Controller
helm uninstall ingress-nginx -n ingress-nginx
kubectl delete namespace ingress-nginx
Remove local-path Provisioner
kubectl delete -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
9. CI/CD — Jenkins Multibranch Pipeline
This project ships with a Jenkinsfile at the repo root that automates the full
build → test → push → deploy lifecycle.
9.1 Prerequisites on the Jenkins Server
| Tool | Purpose | Install |
|---|---|---|
| Docker | Build & push images | apt install docker.io + add jenkins user to docker group |
| kubectl | Apply k8s manifests | Official install guide |
| kustomize | Render overlays | `curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" |
| NodeJS (v18+) | npm ci / npm test |
Jenkins NodeJS plugin or system package |
# Add jenkins user to docker group (run on Jenkins host, then restart Jenkins)
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
9.2 Jenkins Plugins Required
Install via Manage Jenkins → Plugins:
| Plugin | Purpose |
|---|---|
| Multibranch Pipeline | Detects branches + PRs automatically |
| Docker Pipeline | Docker credential binding |
| Credentials Binding | Injects secrets into pipeline steps |
| NodeJS | Managed Node.js installations |
| Git | SCM checkout |
9.3 Configure Jenkins Credentials
Go to Manage Jenkins → Credentials → System → Global credentials → Add:
| Credential ID | Kind | Fields | Used for |
|---|---|---|---|
dockerhub-credentials |
Username with password | Username: subkamble / Password: Docker Hub PAT |
docker push |
kubeconfig-secret |
Secret file | Upload your ~/.kube/config |
kubectl cluster access |
Docker Hub PAT: Generate at hub.docker.com → Account Settings → Security → New Access Token (Read/Write scope).
kubeconfig tip: If Jenkins runs inside the same cluster, use a ServiceAccount + RBAC instead of a kubeconfig file.
9.4 Create the Multibranch Pipeline Job
- Jenkins Dashboard → New Item
- Name:
react-mysql→ select Multibranch Pipeline → OK - Under Branch Sources → Add source → Git
- Repository URL: your repo URL
- Credentials: add repo access if private
- Under Build Configuration → Mode: by Jenkinsfile → Script Path:
Jenkinsfile - Under Scan Multibranch Pipeline Triggers → enable Periodically if not otherwise run (e.g. every 5 min), or configure a webhook (preferred)
- Save → Scan Multibranch Pipeline Now
Jenkins will discover all branches and create sub-jobs automatically.
9.5 Webhook Setup (recommended — avoids polling)
GitHub:
- Repo → Settings → Webhooks → Add webhook
- Payload URL:
http://<JENKINS_URL>/multibranch-webhook-trigger/invoke?token=react-mysql - Content type:
application/json - Events: Push + Pull requests
Install the Multibranch Scan Webhook Trigger plugin on Jenkins to handle the token.
9.6 Branch Behavior Summary
| Branch | Build | Test | Push Image | Deploy |
|---|---|---|---|---|
main |
Yes | Yes | Yes (latest + SHA tag) |
k8s/overlays/onpremise (production) |
staging |
Yes | Yes | Yes (branch-SHA tag) | k8s/overlays/minikube (staging) |
develop |
Yes | Yes | No | No |
feature/** |
Yes | Yes | No | No |
| PR branches | Yes | Yes | No | No |
9.7 Image Tagging Convention
| Branch | Tag format | Example |
|---|---|---|
main |
latest + <sha8> |
latest, a1b2c3d4 |
staging |
staging-<sha8> |
staging-a1b2c3d4 |
feature/login |
feature-login-<sha8> |
feature-login-a1b2c3d4 |
9.8 First-Run Checklist
# Verify Jenkins can reach Docker Hub
docker login -u subkamble
# Verify Jenkins can reach the cluster
kubectl get nodes
# Verify kustomize is on PATH
kustomize version
# Trigger a manual build for main branch from Jenkins UI
# then watch the Console Output for each stage
9.9 Rollback
# List available image tags (via Docker Hub API or local docker images)
docker images subkamble/react-mysql-backend
# Pin a specific tag manually and re-deploy
cd k8s/base
kustomize edit set image \
subkamble/react-mysql-backend=subkamble/react-mysql-backend:<previous-sha> \
subkamble/react-mysql-frontend=subkamble/react-mysql-frontend:<previous-sha>
cd -
kubectl apply -k k8s/overlays/onpremise
kubectl rollout status deployment/backend -n react-mysql
kubectl rollout status deployment/frontend -n react-mysql
10. GitOps — ArgoCD (Reference)
The Jenkinsfile contains fully commented-out ArgoCD pipeline stages. Follow this section to activate GitOps mode.
10.1 Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for all ArgoCD pods to be ready
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=180s
# Expose the ArgoCD API server (choose one):
# Option A — Port-forward (local access only)
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Option B — LoadBalancer (on-prem with MetalLB or cloud)
kubectl patch svc argocd-server -n argocd \
-p '{"spec":{"type":"LoadBalancer"}}'
# Option C — Ingress (add argocd.myapp.local to your Ingress controller)
10.2 Get Initial Admin Password
kubectl get secret argocd-initial-admin-secret \
-n argocd \
-o jsonpath="{.data.password}" | base64 -d && echo
# Login
argocd login localhost:8080 --username admin --password <above-password> --insecure
# Change password immediately
argocd account update-password
10.3 Apply ArgoCD Application Manifests
Create the two Application objects (one per environment).
These are documented in full inside the Jenkinsfile comments.
Here is the summary of what they do:
| Application | Watches branch | Overlay | Sync policy |
|---|---|---|---|
react-mysql-production |
main |
k8s/overlays/onpremise |
Automated (prune + selfHeal) |
react-mysql-staging |
staging |
k8s/overlays/minikube |
Automated (prune + selfHeal) |
# Copy the YAML blocks from the Jenkinsfile comments into files, then:
kubectl apply -f argocd/application-production.yaml
kubectl apply -f argocd/application-staging.yaml
# Verify apps are registered
argocd app list
10.4 Jenkins Credentials for GitOps Mode
Add these alongside the existing credentials:
| Credential ID | Kind | Purpose |
|---|---|---|
argocd-auth-token |
Secret text | ArgoCD API token for argocd app sync |
gitops-ssh-key |
SSH username with private key | Push image tag updates back to Git |
Generate an ArgoCD API token:
argocd account generate-token --account admin
# Copy the output → paste as the 'argocd-auth-token' secret text
10.5 Activate GitOps in the Jenkinsfile
- Open
Jenkinsfile - Remove the active Deploy and Smoke Test stages (or leave them as fallback)
- Uncomment the
Update GitOps ManifestandArgoCD Syncstage blocks in the GitOps section at the bottom - Replace
YOUR_ARGOCD_SERVERwith your ArgoCD server hostname/IP - Replace
YOUR_ORGin the git clone URL with your GitHub org/username - Commit and push — Jenkins picks up the change automatically
10.6 GitOps Flow Diagram
Developer pushes code
│
▼
Jenkins CI
┌─────────────────────────────────────┐
│ 1. npm ci → test │
│ 2. docker build + push :<sha-tag> │
│ 3. kustomize edit set image │
│ 4. git commit + push [skip ci] │
└─────────────────┬───────────────────┘
│ Git push (image tag bump)
▼
Git Repository
(k8s/base/kustomization.yaml updated)
│
│ ArgoCD polls every 3 min
│ (or Jenkins triggers sync via argocd CLI)
▼
ArgoCD detects drift
┌────────────────────────────┐
│ kubectl apply -k overlay/ │
│ prune removed resources │
│ self-heal manual changes │
└────────────┬───────────────┘
│
▼
Kubernetes Cluster
(react-mysql namespace updated)
10.7 ArgoCD Quick Reference
# View app status
argocd app get react-mysql-production
# Force immediate sync (skip waiting for poll interval)
argocd app sync react-mysql-production
# Rollback to previous revision
argocd app history react-mysql-production
argocd app rollback react-mysql-production <revision-id>
# Pause auto-sync (for maintenance)
argocd app set react-mysql-production --sync-policy none
# Resume auto-sync
argocd app set react-mysql-production --sync-policy automated
# Delete app (does NOT delete k8s resources by default)
argocd app delete react-mysql-production
# Delete app AND k8s resources
argocd app delete react-mysql-production --cascade
Quick Reference Card
# Deploy
kubectl apply -k k8s/overlays/onpremise
# Check status
kubectl get pods,svc,ingress,pvc -n react-mysql
# Stream all logs
kubectl logs -n react-mysql -l app=mysql -f
kubectl logs -n react-mysql -l app=backend -f
kubectl logs -n react-mysql -l app=frontend -f
# Debug a crashing pod
kubectl describe pod <pod-name> -n react-mysql
kubectl logs <pod-name> -n react-mysql --previous
# Test API directly (bypass ingress)
kubectl port-forward svc/backend 3000:3000 -n react-mysql
curl localhost:3000/user
# MySQL shell
kubectl exec -it mysql-0 -n react-mysql -- mysql -u root -ppass123 appdb
# Re-apply after manifest changes
kubectl apply -k k8s/overlays/onpremise
# Full teardown
kubectl delete namespace react-mysql