Creating Your First App
A complete walkthrough — from empty cluster to running application
Creating Your First App
This tutorial walks you through deploying a real application with Wardn — from an empty cluster to a running, healthy application with automated sync.
Needs a Kubernetes cluster. The App CR + operator only run against a real cluster — use docker-compose.k8s.yml (k3s) from Getting Started before starting this tutorial.
What You'll Build
A simple web application with:
- A Deployment running 3 replicas
- A Service exposing it internally
- A ConfigMap with application configuration
- Automated sync with pruning enabled
Prerequisites
- Wardn server + operator running (Getting Started)
kubectlconfigured against the cluster (k3s.yamlif using the compose layer)- A Git repository you can push to — public for now, since the operator's clone has no credential support
Tutorial
Step 1: Create the Application Repo
Create a new Git repository with the following structure:
my-first-app/
└── manifests/
├── deployment.yaml
├── service.yaml
└── configmap.yamlmanifests/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-first-app
labels:
app: my-first-app
spec:
replicas: 3
selector:
matchLabels:
app: my-first-app
template:
metadata:
labels:
app: my-first-app
spec:
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: my-first-app-configmanifests/service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-first-app
spec:
selector:
app: my-first-app
ports:
- port: 80
targetPort: 80manifests/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-first-app-config
data:
APP_ENV: "production"
LOG_LEVEL: "info"Step 2: Push to Git
cd my-first-app
git init
git add .
git commit -m "Initial manifests"
git remote add origin https://github.com/yourorg/my-first-app.git
git push -u origin mainUsing the bundled Gitea instead of GitHub:
docker compose -f docker-compose.k8s.yml exec gitea \
gitea admin user create --username wardn --password wardn1234 --email dev@wardn.local --admin
# Create a repo in the Gitea UI at http://localhost:3000's gitea port, then
git remote add origin http://localhost:3000/wardn/my-first-app.gitStep 3: Create the App
Create app.yaml:
apiVersion: cd.wardn.space/v1alpha1
kind: App
metadata:
name: my-first-app
namespace: org-my-org # the org's management namespace
spec:
source:
type: Git
git:
repoURL: https://github.com/yourorg/my-first-app
branch: main
path: manifests
manifestType: Auto
destination:
namespace: default
syncPolicy:
automated: true
prune: trueApply it:
kubectl --kubeconfig k3s.yaml apply -f app.yamlStep 4: Watch the Sync
kubectl --kubeconfig k3s.yaml get app my-first-app -n org-my-org -wWithin ~60 seconds (or immediately if a webhook is wired), you should see:
NAME SYNC STATUS HEALTH
my-first-app OutOfSync Unknown
my-first-app Synced HealthyStep 5: Verify the Deployment
kubectl --kubeconfig k3s.yaml get pods -n default -l app=my-first-app
kubectl --kubeconfig k3s.yaml get svc -n default my-first-appExpected output:
NAME READY STATUS RESTARTS AGE
my-first-app-7d6f8c5b-x2k9m 1/1 Running 0 30s
my-first-app-7d6f8c5b-p3rlq 1/1 Running 0 30s
my-first-app-7d6f8c5b-n8w2x 1/1 Running 0 30sStep 6: Deploy a Change
Update the image tag in manifests/deployment.yaml:
image: nginx:1.26Commit and push:
git add manifests/deployment.yaml
git commit -m "Upgrade nginx to 1.26"
git pushWith automated: true, the next reconcile (≤ ~60s) picks up the new sha and applies it:
kubectl --kubeconfig k3s.yaml get app my-first-app -n org-my-org -wStep 7: Trigger a Manual Sync (optional)
curl -X POST http://localhost:8080/api/v1/apps/default/my-first-app/sync -b cookies.txt
# {"message":"sync triggered","name":"my-first-app","operation_id":"…"}What Just Happened?
- You pushed to Git — the single source of truth changed
- The operator detected the new revision — on its ~60s reconcile (or instantly via webhook)
- Manifests were rendered —
.yamlfiles underpathparsed to objects - Server-side apply — objects applied with field manager
wardn-operator - Health checks ran — Deployment readiness drives
healthStatus - Status updated —
syncStatus/revision/objectHashwritten back to theAppCR
About Self-Heal
The syncPolicy.selfHeal field exists in the schema but isn't wired in the reconciler yet. If you kubectl scale the deployment down, Wardn won't restore it until the next upstream change (or a manual sync). This is one of the biggest gaps vs ArgoCD — track it on the Roadmap.
Troubleshooting
Sync Status is Unknown / OutOfSync forever
Check the operator logs:
kubectl --kubeconfig k3s.yaml logs -n <operator-namespace> deployment/wardn-operator --tail=50Common causes:
- Repo URL unreachable or private (no credential support — public repos only)
- Branch doesn't exist (
mainvsmaster) pathdoesn't contain any manifestsmanifestType: Helm/HelmChart/Catalogsource — not implemented, the reconcile fails by design
Health Status is Degraded or stuck Progressing
Check per-resource status:
kubectl --kubeconfig k3s.yaml get app my-first-app -n org-my-org \
-o jsonpath='{.status.resources}' | jqCommon causes:
- Image pull errors (wrong name, missing
pullSecretRef) - Crash loops (
kubectl logson the pod) readyReplicasnot reachingspec.replicaswithin the Deployment's progress deadline
Sync Doesn't Trigger
- Verify
syncPolicy.automatedistrue(or trigger manually viaPOST /sync) - Check
spec.freezeisn'ttrue - Check the operator is running and can reach the source cluster
Next Steps
- Multi-Cluster Deployment — deploy to a registered remote cluster via
destination.cluster - Kustomize — overlays instead of raw YAML
- Rollback — roll back to a previous revision
- Webhook Triggers — instant syncs on push