ArgoCD
GitOps continuous delivery controller for the homelab Kubernetes cluster. ArgoCD watches the GitHub repository and automatically synchronizes cluster state to match the manifests in main.
Last reviewed: March 11, 2026 — documentation is up-to-date with current cluster state.
How It Works
ArgoCD is bootstrapped by Terraform (not by kubectl apply -k). Terraform installs the ArgoCD Helm chart and creates the root Application CR (argocd-apps) that triggers the App of Apps pattern. From that point on, git pushes automatically deploy to the cluster.
flowchart TD
subgraph terraform["Terraform (bootstrap, run once)"]
TF["terraform apply"]
TF --> HelmRelease["helm_release: argocd\nargo-cd v7.8.0\nNodePort :30080"]
TF --> RootApp["Application: argocd-apps\n→ k8s/apps/argocd/"]
TF --> InfisicalApp["Application: infisical\n(Helm values embedded in Terraform)"]
end
subgraph git["GitHub: holdennguyen/homelab"]
ArgoDir["k8s/apps/argocd/\nkustomization.yaml + Application CRs"]
end
subgraph cluster["argocd namespace"]
ArgoServer["argocd-server\n:30080 HTTP (insecure mode)"]
AppController["application-controller"]
RepoServer["repo-server"]
RootApp -- "watches" --> ArgoDir
ArgoDir -- "creates" --> ESOApp["Application: external-secrets"]
ArgoDir -- "creates" --> ESCApp["Application: external-secrets-config"]
ArgoDir -- "creates" --> MonApp["Application: monitoring"]
ArgoDir -- "creates" --> AuthApp["Application: authentik"]
ArgoDir -- "creates" --> OCApp["Application: openclaw"]
ArgoDir -- "creates" --> TrivyApp["Application: trivy-operator"]
ArgoDir -- "creates" --> TrivyScannerApp["Application: trivy-operator-vulnerability-scanner"]
ArgoDir -- "creates" --> TrivyDashApp["Application: trivy-dashboard"]
ArgoDir -- "creates" --> NSApp["Application: namespace-security"]
ArgoDir -- "creates" --> NPApp["Application: networking-policies"]
ArgoDir -- "creates" --> LFApp["Application: launchfast"]
end
AppController -- "poll every ~3min" --> git
Directory Contents
| File | Purpose |
|---|---|
kustomization.yaml |
Lists AppProjects and Application CRs managed by the App of Apps |
projects/secrets.yaml |
AppProject for secret management infrastructure |
projects/data.yaml |
AppProject for databases and data stores |
projects/apps.yaml |
AppProject for user-facing applications |
applications/external-secrets-app.yaml |
ESO Helm chart (sync-wave 0 — installs CRDs first) |
applications/external-secrets-config-app.yaml |
ClusterSecretStore (sync-wave 1 — after ESO CRDs) |
applications/monitoring-app.yaml |
kube-prometheus-stack Helm chart (Grafana + Prometheus) |
applications/monitoring-config-app.yaml |
Grafana ExternalSecret (monitoring namespace) |
applications/authentik-app.yaml |
Authentik SSO Helm chart |
applications/authentik-config-app.yaml |
Authentik ExternalSecret (authentik namespace) |
applications/openclaw-app.yaml |
OpenClaw AI gateway deployment |
applications/trivy-operator-app.yaml |
Trivy vulnerability scanner Helm chart (monitoring namespace, ClientServer mode) |
applications/trivy-dashboard-app.yaml |
Trivy Operator Dashboard web UI (trivy-dashboard namespace) |
applications/trivy-operator-vulnerability-scanner-app.yaml |
Trivy VulnerabilityScanner CR for scheduled daily scans |
applications/namespace-security-app.yaml |
Pod Security Standard labels for namespaces |
applications/networking-policies-app.yaml |
Default-deny NetworkPolicies across all namespaces |
applications/launchfast-app.yaml |
LaunchFast landing page (launchfast namespace) |
Note: The
infisicalApplication CR is not in this directory. It is created byterraform/argocd.tfbecause its Helm values include sensitive PostgreSQL and Redis passwords that cannot be stored in git.
App of Apps Pattern
The root Application (argocd-apps) watches k8s/apps/argocd/. Any AppProject or Application CR added to that directory (and listed in kustomization.yaml) is automatically deployed by ArgoCD.
flowchart LR
subgraph root["argocd-apps (root Application, default project)"]
Kustomize["kustomization.yaml"]
end
subgraph secretsProj["secrets project"]
Infisical["infisical\n(Terraform-managed)"]
A1["external-secrets\n(sync-wave 0)"]
A2["external-secrets-config\n(sync-wave 1)"]
end
subgraph dataProj["data project"]
A3["(reserved)"]
end
subgraph appsProj["apps project"]
A5["monitoring"]
A6["authentik"]
A7["openclaw"]
A8["trivy-operator"]
A11["trivy-dashboard"]
A9["namespace-security"]
A10["networking-policies"]
A13["launchfast"]
end
Kustomize --> secretsProj
Kustomize --> dataProj
Kustomize --> appsProj
Sync Wave Ordering
Sync waves control the order in which resources are applied. AppProjects must exist before Applications reference them.
| Wave | Resource | Why |
|---|---|---|
| -1 | AppProjects (secrets, data, apps) |
Must exist before any Application references them |
| 0 | external-secrets |
Installs the ESO Helm chart + CRDs (ExternalSecret, ClusterSecretStore, etc.) |
| 1 | external-secrets-config |
Applies the ClusterSecretStore — requires CRDs from wave 0 to be present |
| 2 | trivy-operator |
Vulnerability scanner — after core apps are synced |
| (default) | monitoring, authentik, openclaw, namespace-security, networking-policies |
No ordering requirements between them |
ArgoCD Configuration
ArgoCD runs in insecure mode (server.insecure = true) — it serves plain HTTP on port 8080/30080. TLS is terminated by Tailscale Serve, which provides a valid Let's Encrypt certificate. This avoids the need for cert-manager or self-signed certificates inside the cluster.
| Setting | Value | Set via |
|---|---|---|
server.service.type |
NodePort |
Terraform Helm values |
server.service.nodePorts.http |
30080 |
Terraform Helm values |
configs.params.server.insecure |
true |
Terraform Helm values |
configs.cm.oidc.config |
Authentik OIDC | Terraform Helm values (argocd_oidc_client_secret tfvar) |
configs.rbac.policy.default |
role:admin |
Terraform Helm values — all SSO users get admin access |
configs.cm.admin.enabled |
false |
Terraform Helm values — local admin login disabled |
configs.cm.resource.customizations.ignoreDifferences.external-secrets.io_ExternalSecret |
jsonPointers: [/metadata/finalizers] |
Terraform Helm values — prevents ESO finalizer from causing permanent OutOfSync |
| Chart version | 7.8.0 |
terraform/variables.tf default |
Repository Authentication
The homelab repository is public on GitHub. ArgoCD clones it via unauthenticated HTTPS — no deploy keys, PATs, or credentials are needed. All Application CRs use the HTTPS URL format:
repoURL: https://github.com/holdennguyen/homelab.git
This eliminates the risk of SSH private key leakage in the public repo's Terraform state or tfvars. If the repo ever goes private, a Fine-grained PAT can be added via the Infisical → ESO pipeline.
Adding a New Application
Projects
Every Application must belong to an AppProject. Pick the project that matches the service's role:
| Project | Purpose | When to use |
|---|---|---|
secrets |
Secret management infrastructure | Secret stores, secret operators, certificate managers |
data |
Databases and persistent data stores | PostgreSQL, Redis, object storage, message queues |
apps |
User-facing applications and services | Web apps, APIs, dashboards, developer tools |
default |
Bootstrap only | Reserved for the argocd-apps root Application |
If a new application's destination namespace is not already listed in the project's destinations, add it to the corresponding projects/*.yaml file. Similarly, if it deploys cluster-scoped resources (CRDs, ClusterRoles, etc.), add them to clusterResourceWhitelist.
Labels
Every Application CR must carry these four standard Kubernetes labels:
| Label | Value | Rule |
|---|---|---|
app.kubernetes.io/name |
Application name | Must match metadata.name |
app.kubernetes.io/part-of |
homelab |
Always homelab |
app.kubernetes.io/component |
Functional role | One of: secrets, database, git, dashboard, ai, gitops, or a new descriptive value |
app.kubernetes.io/managed-by |
argocd |
Always argocd |
Do not add custom-prefixed labels (e.g., homelab/*). Use only app.kubernetes.io/* labels to stay consistent with the Kubernetes recommended labels convention.
Template
- Create a directory
k8s/apps/my-service/withkustomization.yamland resource manifests - Create
k8s/apps/argocd/applications/my-service-app.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-service
namespace: argocd
labels:
app.kubernetes.io/name: my-service
app.kubernetes.io/part-of: homelab
app.kubernetes.io/component: <component> # e.g. database, monitoring
app.kubernetes.io/managed-by: argocd
spec:
project: <project> # secrets | data | apps
source:
repoURL: https://github.com/holdennguyen/homelab.git
targetRevision: HEAD
path: k8s/apps/my-service
destination:
server: https://kubernetes.default.svc
namespace: my-service
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- If the destination namespace is not in the project's
destinations, add it to the project file inprojects/ - Add the Application to
k8s/apps/argocd/kustomization.yaml:
resources:
- applications/my-service-app.yaml
- Push to
main. ArgoCD detects the change within ~3 minutes and deploys.
Checklist
- [ ]
metadata.namematches the service name - [ ]
metadata.labelsincludes all fourapp.kubernetes.io/*labels - [ ]
spec.projectis set tosecrets,data, orapps(neverdefault) - [ ] Destination namespace is allowed in the project's
destinations - [ ] Any cluster-scoped resources are allowed in the project's
clusterResourceWhitelist - [ ] Application file is listed in
kustomization.yaml
Accessing the UI
ArgoCD is accessible at https://holdens-mac-mini.story-larch.ts.net:8443 from any Tailscale device. Authentication is handled exclusively via Authentik SSO (OIDC) — the local admin login is disabled.
One-time Tailscale Serve setup:
tailscale serve --bg --https 8443 http://localhost:30080
Authentication:
Click "Log in via Authentik" on the ArgoCD login page. All SSO-authenticated users receive role:admin access (single-user homelab). The OIDC configuration is managed in terraform/argocd.tf.
Operational Commands
# Check application status
kubectl get applications -n argocd
# Check all ArgoCD pods
kubectl get pods -n argocd
# Force an immediate sync on a specific application
kubectl patch application openclaw -n argocd \
--type merge -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}'
# View ArgoCD server logs
kubectl logs -n argocd deploy/argocd-server --tail=50
# View application controller logs
kubectl logs -n argocd deploy/argocd-application-controller --tail=50
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
App shows OutOfSync forever |
ArgoCD can't clone repo | Verify the HTTPS URL is reachable: git ls-remote https://github.com/holdennguyen/homelab.git |
Application stuck in Progressing |
Pod not ready | kubectl describe pod -n <namespace> for Events |
| CRD not found during sync | Wrong sync wave order | Ensure external-secrets (wave 0) is healthy before external-secrets-config (wave 1) syncs |
| Changes not deployed after push | Normal poll delay | Wait ~3min or force refresh via annotation |
kubernetes_manifest schema error |
Not applicable | ArgoCD Application CRs are applied via local-exec in Terraform, not kubernetes_manifest |