GitOps treats your Git repository as the single source of truth for infrastructure and application state. Every change goes through a pull request, every deployment is auditable, and drift from the desired state is detected and corrected automatically. For small teams of 2-8 engineers, GitOps eliminates the "who deployed what and when" problem entirely.
ArgoCD is the most widely adopted GitOps controller for Kubernetes, used by 62% of GitOps practitioners according to the 2025 CNCF survey. Combined with Kustomize for environment overlays, it provides a complete deployment system that requires zero custom scripting. This guide covers the full setup from repository structure through production rollback.
---
Repository Structure with Kustomize
Kustomize uses a base-plus-overlays pattern. The base directory contains resources common to all environments. Each overlay directory patches the base for a specific environment (staging, production). This eliminates the copy-paste problem of maintaining separate YAML files per environment.
k8s/
base/
deployment.yaml
service.yaml
ingress.yaml
kustomization.yaml
overlays/
staging/
kustomization.yaml
replica-patch.yaml
production/
kustomization.yaml
replica-patch.yaml
ingress-patch.yaml
---
Base Resources
The base `kustomization.yaml` declares all shared resources:
<h1>k8s/base/kustomization.yaml</h1>
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
commonLabels:
app: my-app
managed-by: kustomize
<h1>k8s/base/deployment.yaml</h1>
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: ghcr.io/my-org/my-app:latest
ports:
- containerPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
<h1>k8s/base/service.yaml</h1>
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 3000
---
Environment Overlays
Overlays patch the base resources for each environment. Staging uses fewer replicas and lower resource limits. Production uses more replicas, custom ingress rules, and tighter resource constraints.
<h1>k8s/overlays/staging/kustomization.yaml</h1>
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
resources:
- ../../base
patches:
- path: replica-patch.yaml
images:
- name: ghcr.io/my-org/my-app
newTag: staging-latest
<h1>k8s/overlays/staging/replica-patch.yaml</h1>
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
<h1>k8s/overlays/production/kustomization.yaml</h1>
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- ../../base
patches:
- path: replica-patch.yaml
- path: ingress-patch.yaml
images:
- name: ghcr.io/my-org/my-app
newTag: v1.2.0
<h1>k8s/overlays/production/replica-patch.yaml</h1>
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
---
ArgoCD Application Configuration
ArgoCD watches your Git repository and syncs the desired state to the cluster. The Application resource tells ArgoCD which repo, path, and branch to monitor.
<h1>argocd/staging-app.yaml</h1>
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-staging
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/my-app.git
targetRevision: main
path: k8s/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: staging
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
<h1>argocd/production-app.yaml</h1>
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-production
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/my-app.git
targetRevision: main
path: k8s/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: false
syncOptions:
- CreateNamespace=true
The key difference between environments: staging has `selfHeal: true` (auto-corrects drift), while production has `selfHeal: false` (alerts on drift but requires manual sync). This prevents accidental production changes from auto-reverting during an active incident response.
---
PR-Based Promotion Workflow
Promotion from staging to production follows a strict PR workflow:
<h1>Automated image tag update script</h1>
#!/bin/bash
NEW_TAG=$1
ENVIRONMENT=$2
cd k8s/overlays/$ENVIRONMENT
<h1>Update image tag in kustomization.yaml</h1>
kustomize edit set image ghcr.io/my-org/my-app:$NEW_TAG
<h1>Commit and push</h1>
git add kustomization.yaml
git commit -m "deploy($ENVIRONMENT): update image to $NEW_TAG"
git push origin main
This script can run as the final step of your CI pipeline for staging, and as a manual step (or PR) for production. The Git history becomes a complete deployment log.
---
Drift Detection and Self-Healing
Drift occurs when someone applies a manual `kubectl` change that differs from the Git repository state. ArgoCD detects drift in two ways:
- Periodic reconciliation: ArgoCD compares the live cluster state to the Git repository every 3 minutes (configurable)
- Webhook-based: ArgoCD receives webhook notifications on Git push and immediately reconciles
| Drift Type | Detection | Auto-Heal | Action Required |
| Replica count changed manually | 3 minutes | Yes (staging) | None |
| ConfigMap modified via kubectl | 3 minutes | Yes (staging) | None |
| Image tag changed manually | 3 minutes | No (production) | Manual sync |
| New resource added manually | 3 minutes | Pruned | None |
To configure webhook-based drift detection for faster response:
<h1>Add webhook to your GitHub repository</h1>
<h1>Settings > Webhooks > Add webhook</h1>
<h1>Payload URL: https://argocd.yourdomain.com/api/webhook</h1>
<h1>Content type: application/json</h1>
<h1>Events: Push events only</h1>
---
Rollback Procedures
GitOps makes rollback trivial because every deployment is a Git commit. Rolling back means reverting to a previous commit.
<h1>Option 1: Git revert (creates a new forward commit)</h1>
git revert HEAD
git push origin main
<h1>Option 2: ArgoCD CLI rollback to previous sync</h1>
argocd app rollback my-app-production
<h1>Option 3: ArgoCD UI</h1>
<h1>Applications > my-app-production > History > Select revision > Rollback</h1>
Git revert is preferred because it maintains a clean audit trail. ArgoCD rollback is faster (immediate) but requires manual Git cleanup afterward to prevent the next sync from re-deploying the broken version.
---
Monitoring ArgoCD Health
<h1>Check application sync status</h1>
argocd app list
<h1>Get detailed sync status and health</h1>
argocd app get my-app-production
<h1>View sync history</h1>
argocd app history my-app-production
<h1>Check for out-of-sync resources</h1>
argocd app diff my-app-production
Set up Prometheus alerts for ArgoCD sync failures. ArgoCD exposes metrics at `/metrics` that Prometheus can scrape. Key metrics to alert on: `argocd_app_info` with `sync_status="OutOfSync"` and `health_status="Degraded"`.
For setting up the complete monitoring stack, see our VPS monitoring and alerting guide. If you need canary deployments alongside GitOps, check our canary deployment best practices. Teams building their first CI pipeline should start with our CI/CD pipeline guide for startups.
---
Frequently Asked Questions
How many repositories should a small team use for GitOps?
Start with a monorepo containing both application code and Kubernetes manifests in separate directories. A 2-5 person team gains no benefit from separate app and config repositories. Split into separate repos only when you have dedicated platform engineers managing infrastructure independently from application developers.
Should I use Kustomize or Helm with ArgoCD?
Kustomize for teams that want simplicity and readability. Helm for teams that need templating for many similar services. Kustomize patches are pure YAML with no templating language to learn. Helm requires understanding Go templates, values files, and chart lifecycle. For most small teams deploying 1-10 services, Kustomize is sufficient and easier to debug.
How do I handle secrets in GitOps?
Use Sealed Secrets (Bitnami) or External Secrets Operator. Sealed Secrets encrypts secrets with a cluster-specific key so encrypted values can be committed to Git safely. External Secrets Operator fetches secrets from AWS Secrets Manager, HashiCorp Vault, or similar services at runtime. Never commit plain Kubernetes Secret manifests to Git.
What if ArgoCD goes down?
Your running applications are unaffected because they run independently of ArgoCD. ArgoCD is a control plane component, not a data plane component. When ArgoCD recovers, it reconciles the current state against the Git repository and applies any pending changes. For high availability, run ArgoCD with 3+ replicas across availability zones.
How do I handle database migrations with GitOps?
Run migrations as Kubernetes Jobs triggered before the main Deployment rolls out. Use ArgoCD sync waves to control ordering: wave 0 for migrations, wave 1 for the application Deployment. The migration Job runs to completion, then ArgoCD proceeds to update the application.
Can I use GitOps without Kubernetes?
GitOps principles (declarative config, Git as source of truth, automated reconciliation) apply to any infrastructure. Tools like Terraform with Atlantis provide GitOps workflows for cloud resources. For VPS deployments, you can implement a lightweight GitOps pattern using webhooks and deploy scripts as described in our one-click GitHub deploy guide.