Deploy ArgoCD on Bare-Metal Kubernetes¶
ArgoCD is a declarative, GitOps-based continuous delivery tool for Kubernetes. It watches a Git repository and automatically reconciles the live cluster state with the desired state defined in Git — no manual kubectl apply needed after the initial setup.
This runbook covers:
- Installing ArgoCD on a bare-metal Kubernetes cluster via Helm
- Exposing the ArgoCD UI (two methods: iximiuz lab port expose and Cloudflare Tunnel)
- Deploying a full microservices application using an ArgoCD
Applicationmanifest, with all key decisions explained - Exposing the deployed app to the internet
- Verifying the deployment via the ArgoCD UI
Prerequisites¶
- A running Kubernetes cluster with
kubectlconfigured. For k3s setup, see: Bootstrap k3s Cluster helminstalled and available inPATH- A Cloudflare tunnel token provisioned (only for Option B — custom domain access)
Dev Machine¶
I use SilverStack Dev Machine — a custom root filesystem on iximiuz Labs, which I maintain with all DevOps tools pre-installed (kubectl, helm, cloudflared, terraform, aws cli, etc.). No local machine setup is required.
Step 1 — Install ArgoCD¶
kubectl create namespace argocd
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update argo
helm install argocd argo/argo-cd \
--namespace argocd
Verify all components are running:
| Component | Kind | Purpose |
|---|---|---|
argocd-server | Deployment | API server + Web UI |
argocd-repo-server | Deployment | Clones Git repos, renders manifests (Helm/Kustomize/raw YAML) |
argocd-application-controller | StatefulSet | Reconciles desired (Git) vs live (cluster) state |
argocd-applicationset-controller | Deployment | Generates Application objects from templates |
argocd-dex-server | Deployment | OIDC SSO provider |
argocd-redis | Deployment | Caching layer for repo server and app controller |
argocd-notifications-controller | Deployment | Sends sync/health event notifications |
Retrieve the initial admin password:
kubectl get secret argocd-initial-admin-secret \
-n argocd \
-o jsonpath="{.data.password}" | base64 --decode; echo
Delete the initial secret
Delete this secret after the first login and password change.
Step 2 — Expose the ArgoCD UI¶
By default, argocd-server is a ClusterIP service. On bare-metal there is no cloud load balancer, so patch it to NodePort first:
kubectl patch svc argocd-server -n argocd \
-p '{"spec":{"type":"NodePort"}}'
kubectl get svc argocd-server -n argocd
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
argocd-server NodePort 10.43.86.5 <none> 80:30340/TCP,443:30440/TCP 4m37s
ArgoCD now listens on NodePort 30340 (HTTP) and 30440 (HTTPS). Choose one of the two access methods below.
Option A — iximiuz Lab Port Expose¶
In the iximiuz lab UI, click Expose HTTP(S) Ports:
- Port:
30440 - HTTPS: ON
- Click EXPOSE
A public URL like https://6a...ae0c2.node-ap-b1d4.iximiuz.com is generated.
Why port 30440 and not 30340?
ArgoCD enforces HTTPS redirects — connecting over plain HTTP on 30340 immediately redirects to HTTPS. Use the HTTPS NodePort directly.
Option B — Cloudflare Tunnel (Custom Domain)¶
For full Cloudflare Tunnel setup, see: Creating Cloudflare Tunnels
In the Cloudflare dashboard (Zero Trust → Networks → Tunnels → your tunnel → Public Hostnames), add a route:
| Field | Value |
|---|---|
| Subdomain | argocd |
| Domain | <your-domain> |
| Service Type | HTTPS |
| Service URL | localhost:30440 |
| No TLS Verify | ON |
Why HTTPS + No TLS Verify?
ArgoCD's server certificate is self-signed. Cloudflare must reach it over HTTPS (because ArgoCD only speaks HTTPS), but cannot verify the certificate chain. No TLS Verify allows the tunnel to connect without a trusted CA.
Step 3 — Clone the Application Repo¶
The application being deployed is Online Boutique — a microservices demo originally by Google. It has been forked to ibtisam-iq/microservices-demo.
About this fork
The upstream repo is the Google Cloud microservices-demo. After forking, the following CI workflows were added:
ci-trigger.yaml— detects which services changed and triggers targeted buildsreusable-build.yaml— builds each service image and pushes it toghcr.io/ibtisam-iq/microservices-demo
The Helm chart at helm-chart/ references these custom images via images.repository and images.tag values. ArgoCD uses this chart as its source in the next step.
Step 4 — Deploy the Application via ArgoCD¶
Create the ArgoCD Application manifest:
cat <<'EOF' > boutique-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: boutique-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/ibtisam-iq/microservices-demo
targetRevision: main
path: helm-chart
helm:
parameters:
- name: images.repository
value: "ghcr.io/ibtisam-iq/microservices-demo"
- name: images.tag
value: "latest"
- name: loadGenerator.create
value: "false"
destination:
server: https://kubernetes.default.svc
namespace: boutique-app
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
EOF
kubectl apply -f boutique-app.yaml
Why images.repository: ghcr.io/ibtisam-iq/microservices-demo?
The upstream Helm chart defaults to Google's own image registry. All service images have been rebuilt and pushed to GitHub Container Registry under this account, so the repository is overridden here to pull from the correct location.
Why images.tag: latest?
The Helm chart defaults to using the chart's appVersion as the image tag. Overriding with latest ensures the most recently pushed image is always pulled — appropriate for this demo setup.
In production, pin to an immutable tag (e.g., a Git commit SHA) for reproducible deployments and reliable rollbacks.
Why loadGenerator.create: false?
The load generator service requires its own custom-built image, which was not pushed to GHCR as part of this setup. Disabling it avoids an ImagePullBackOff error on that pod.
Step 5 — Expose the Frontend¶
The Helm chart creates frontend-external as a LoadBalancer service. On bare-metal without MetalLB, it stays <pending> indefinitely. Patch it to NodePort.
First, disable selfHeal — otherwise ArgoCD will revert the manual patch within seconds:
kubectl patch application boutique-app -n argocd \
--type merge \
-p '{"spec":{"syncPolicy":{"automated":{"selfHeal":false}}}}'
Then patch the service:
kubectl patch svc frontend-external -n boutique-app \
-p '{"spec":{"type":"NodePort","ports":[{"port":80,"targetPort":8080,"nodePort":30080,"protocol":"TCP"}]}}'
kubectl get svc frontend-external -n boutique-app
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-external NodePort 10.43.28.247 <none> 80:30080/TCP 3m
Option A — iximiuz Lab Port Expose¶
In the iximiuz lab UI → Expose HTTP(S) Ports:
- Port:
30080 - HTTPS: OFF (the app serves plain HTTP)
- Click EXPOSE
Option B — Cloudflare Tunnel¶
In the Cloudflare dashboard, add a second public hostname on the same tunnel:
| Field | Value |
|---|---|
| Subdomain | boutique |
| Domain | <your-domain> |
| Service Type | HTTP |
| Service URL | localhost:30080 |
Step 6 — Verify¶
# All 11 microservice pods should be Running
kubectl get po -n boutique-app
# All services should be present
kubectl get svc -n boutique-app
# ArgoCD application status
kubectl get application boutique-app -n argocd
In the ArgoCD UI, the application shows:
- APP HEALTH:
Healthy - SYNC STATUS:
Synced(orOutOfSyncafter the manual service patch — expected, sinceselfHealis now disabled and Git still hasLoadBalancer) - LAST SYNC: timestamp of the last successful reconciliation
Cleanup¶
# Delete the ArgoCD Application (prune will remove all deployed resources)
kubectl delete application boutique-app -n argocd
# Uninstall ArgoCD
helm uninstall argocd -n argocd
kubectl delete namespace argocd
# Tear down k3s (if needed)
/usr/local/bin/k3s-uninstall.sh
Quick Reference¶
# 1. Create namespace and install ArgoCD via Helm
kubectl create namespace argocd
helm repo add argo https://argoproj.github.io/argo-helm && helm repo update argo
helm install argocd argo/argo-cd --namespace argocd
# 2. Verify components
kubectl get po,deploy,sts,svc -n argocd
# 3. Retrieve initial admin password
kubectl get secret argocd-initial-admin-secret -n argocd \
-o jsonpath="{.data.password}" | base64 --decode; echo
# 4. Patch argocd-server to NodePort
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"NodePort"}}'
kubectl get svc argocd-server -n argocd
# 5. Expose ArgoCD UI (Option A: iximiuz — port 30440 HTTPS ON)
# (Option B: Cloudflare Tunnel → subdomain argocd, HTTPS, localhost:30440, No TLS Verify ON)
# 6. Clone the application repo
git clone https://github.com/ibtisam-iq/microservices-demo.git && cd microservices-demo
# 7. Apply the ArgoCD Application manifest
kubectl apply -f boutique-app.yaml
# 8. Disable selfHeal before patching the frontend service
kubectl patch application boutique-app -n argocd \
--type merge -p '{"spec":{"syncPolicy":{"automated":{"selfHeal":false}}}}'
# 9. Patch frontend-external to NodePort
kubectl patch svc frontend-external -n boutique-app \
-p '{"spec":{"type":"NodePort","ports":[{"port":80,"targetPort":8080,"nodePort":30080,"protocol":"TCP"}]}}'
# 10. Expose frontend (Option A: iximiuz — port 30080 HTTPS OFF)
# (Option B: Cloudflare Tunnel → subdomain boutique, HTTP, localhost:30080)
# 11. Verify deployment
kubectl get po -n boutique-app
kubectl get application boutique-app -n argocd
# 12. Cleanup
kubectl delete application boutique-app -n argocd
helm uninstall argocd -n argocd && kubectl delete namespace argocd
Dev Machine
All commands above are run on the SilverStack Dev Machine — no local setup required.