An end-to-end GitOps demo: build a container image once, then promote that exact same artifact through dev → staging → prod with Argo CD. Only configuration changes between environments — never the binary.
In a healthy delivery pipeline you build an artifact once, test it, and then promote that same artifact forward. Rebuilding per environment means each environment runs subtly different bytes — the thing you tested is not the thing you shipped.
This repo makes that principle concrete:
- Built once — CI builds
ghcr.io/gsaini/argocd-getting-started:sha-<commit>and bakes the version + git SHA into the image. That immutable tag is the unit of promotion. - Deployed many — three Kustomize overlays share one base. Promotion is literally copying the same image tag from one overlay to the next.
- Reconciled by Argo CD — each environment is an Argo CD
Application. dev and staging auto-sync; prod waits for a human to click sync.
The running app shows this at a glance: Build version and Git SHA are identical in every environment, while the Environment badge changes.
See docs/architecture.md for the full write-up.
flowchart TD
dev_push["Developer pushes to <code>app/**</code> on <code>main</code>"]
subgraph build["build-once (CI · build.yaml)"]
direction TB
docker["docker build<br/>(bakes VERSION + GIT_SHA)"]
ghcr["push → ghcr.io/gsaini/...:sha-abc123"]
bumpdev["bump k8s/overlays/dev → newTag: sha-abc123"]
docker --> ghcr --> bumpdev
end
subgraph promote["promote (CI · manual · promote.yaml)"]
direction LR
p1["dev tag"] --> p2["staging overlay"] --> p3["prod overlay"]
end
subgraph argocd["Argo CD (reconciles Git → cluster)"]
direction TB
adev["demo-dev · auto-sync"] --> nsdev[["ns argocd-demo-dev"]]
astg["demo-staging · auto-sync"] --> nsstg[["ns argocd-demo-staging"]]
aprod["demo-prod · manual sync"] --> nsprod[["ns argocd-demo-prod"]]
end
dev_push --> build
bumpdev -. git commit .-> adev
build -- "same sha, no rebuild" --> promote
p2 -. git commit .-> astg
p3 -. git commit .-> aprod
The same sha-abc123 image flows through every stage — only the Kustomize
overlay (config, replicas, namespace) changes. dev and staging reconcile
automatically; prod waits for a human to run argocd app sync demo-prod.
app/ # tiny zero-dependency Node web app + Dockerfile
k8s/
base/ # environment-agnostic Deployment + Service + config
overlays/
dev/ # APP_ENV=dev, 1 replica
staging/ # APP_ENV=staging, 2 replicas
prod/ # APP_ENV=prod, 3 replicas, tighter limits
argocd/
project.yaml # AppProject (guardrails: repos, destinations)
applications/ # one Application per environment
app-of-apps.yaml # root app that installs the three Applications
applicationset.yaml # alternative: generate all three from one list
.github/workflows/
build.yaml # build once -> push -> bump dev overlay
promote.yaml # copy the same tag dev -> staging -> prod
validate.yaml # kustomize build on every overlay (PR gate)
scripts/bootstrap.sh # apply the AppProject + app-of-apps
docs/architecture.md
kubectl create namespace argocd
kubectl apply -n argocd -f \
https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml./scripts/bootstrap.sh
# or directly:
kubectl apply -f argocd/project.yaml
kubectl apply -f argocd/app-of-apps.yamlThen watch it reconcile:
kubectl get applications -n argocd
argocd app listkubectl -n argocd-demo-dev port-forward svc/web 8080:80
open http://localhost:8080 # try also /api/info and /healthz-
Push a change to
app/onmain.build.yamlbuilds one image, pushes it to GHCR, and updatesk8s/overlays/devto the newsha-…tag. -
Argo CD auto-syncs dev. The new version is live in
argocd-demo-dev. -
Promote to staging — run the
promoteworkflow with targetstaging(Actions tab → promote → Run workflow). It copies dev's tag into the staging overlay; Argo CD auto-syncs staging. -
Promote to prod — run
promotewith targetprod. It copies staging's tag into the prod overlay. Because prod is manual-sync, release with:argocd app sync demo-prod
No step rebuilds the image — the sha-… string is the only thing moving.
cd app
APP_ENV=local node server.js
# http://localhost:8080Or with Docker, baking build metadata exactly as CI does:
docker build -t argocd-demo ./app \
--build-arg APP_VERSION=1.0.0-local \
--build-arg GIT_SHA="$(git rev-parse --short HEAD)"
docker run --rm -p 8080:8080 -e APP_ENV=local argocd-demoThe manifests reference gsaini as the GitHub owner. If you fork or copy this
repo, replace gsaini with your GitHub username/org in:
k8s/base/kustomization.yamland the three overlays (image name)argocd/**(repo URLs)- the badge/links in this README
grep -rl gsaini . | xargs sed -i '' 's/gsaini/your-name/g' # macOS