GitOps Deployment Pipeline with ArgoCD

Build a GitOps pipeline where ArgoCD reconciles cluster state from Git.

Introduction

30 Second Summary

Every time someone manually tweaks a running cluster, you lose track of what changed, when it changed, and who changed it. Deployments become a guessing game where production drift silently accumulates until something breaks.

In this project, you will build a complete GitOps deployment pipeline where your Git repository becomes the single source of truth for everything running in your Kubernetes cluster. ArgoCD watches for changes and automatically keeps your cluster in sync.

What You'll Build

A production-grade GitOps pipeline where every cluster change flows through a pull request, giving you a complete audit trail and automatic drift correction.

By the end of this project, you'll have:

  • ArgoCD installed and continuously reconciling your cluster state from a Git repository with auto-sync and self-healing enabled.
  • Sealed Secrets encrypting credentials so they can be safely committed to Git alongside your deployment manifests.
  • GitHub Actions and Renovate Bot automating image tag and dependency updates through pull requests with a full audit trail.
  • Secret Mission: Add a PreSync hook that runs a database migration job before deployments, and configure ArgoCD notifications to post deployment status to GitHub commit statuses.

Are there any prerequisites?

This project assumes basic Kubernetes knowledge (pods, deployments, services, namespaces) and Git fundamentals (clone, commit, push, branches, PRs). You should also be comfortable reading YAML files.

You will need Rancher Desktop (which provides k3s and kubectl), Helm, Git, and a GitHub account already set up on your machine.

Install ArgoCD and CLI Tools

Every GitOps pipeline needs an engine that watches your Git repository and continuously applies changes to the cluster. That engine is ArgoCD.

In this step, you will install the ArgoCD CLI on your machine and deploy ArgoCD into your k3s cluster. By the end, you will be logged in and ready to connect a Git repository.

In this step, get ready to:
  • Install the ArgoCD CLI binary.
  • Deploy ArgoCD to your k3s cluster.
  • Log into the ArgoCD server from your terminal.
Install the ArgoCD CLI

The ArgoCD CLI lets you manage applications, trigger syncs, and check status directly from your terminal. You need this binary installed locally before you can interact with the ArgoCD server.

  • Download and install the ArgoCD CLI by running this command:
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 && sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd && rm argocd-linux-amd64

What does this command do?

  • curl -sSL -o argocd-linux-amd64 downloads the latest ArgoCD CLI binary for Linux.
  • sudo install -m 555 moves the binary to /usr/local/bin/argocd and sets it as executable.
  • rm argocd-linux-amd64 cleans up the downloaded file.
  • Confirm the CLI installed correctly by running:
argocd version --client

You should see version information printed with a build date and a Go version. No server connection is needed for this check.

Command not found?

  • Make sure /usr/local/bin is on your PATH. Run echo $PATH to check.
  • If you see a permission error during install, ensure you used sudo in the install command.
  • help me troubleshoot the ArgoCD CLI install on my Linux machine
Install ArgoCD into your cluster

With the CLI ready, you can now deploy ArgoCD itself. ArgoCD runs as a set of pods inside your Kubernetes cluster in a dedicated namespace.

  • Create the argocd namespace and install ArgoCD by running this command:
kubectl create namespace argocd && kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

What does this command do?

kubectl create namespace argocd creates a dedicated namespace for all ArgoCD components.

The kubectl apply pulls the official install manifest and deploys ArgoCD's server, repo server, application controller, Dex server, and Redis instance. The --server-side --force-conflicts flags are required because some ArgoCD CRDs exceed the 262KB annotation size limit imposed by client-side apply.

  • Wait about 60 seconds for the pods to start, then verify they are running:
kubectl get pods -n argocd

You should see pods named argocd-server, argocd-repo-server, argocd-application-controller, argocd-dex-server, and argocd-redis all showing a Running status.

Pods stuck in Pending or CrashLoopBackOff?

  • Run kubectl describe pod <pod-name> -n argocd to see the events and find the root cause.
  • If pods are Pending, your cluster may be low on resources. Check that Rancher Desktop has enough CPU and memory allocated in its preferences.
  • If you see ContainerCreating for more than two minutes, the container images are still downloading. Wait and re-run the command.
  • help me debug ArgoCD pods not starting in my k3s cluster
Port-forward the ArgoCD server and log in

ArgoCD's web UI and API are not exposed outside the cluster by default. You need to forward a local port to the ArgoCD server service so your CLI and browser can reach it.

  • Start the port-forward by running this command:
kubectl port-forward svc/argocd-server -n argocd 8080:443

What does this command do?

This forwards traffic from localhost:8080 on your machine to port 443 on the argocd-server service inside the cluster. The command stays running in the foreground. You need to keep this terminal open.

  • Open a new terminal window. You will run the remaining commands there while the port-forward stays active in the first terminal.
  • Retrieve the auto-generated admin password by running:
argocd admin initial-password -n argocd

The first line of output is your password. Copy it now. You will paste it in the next command.

  • Log into ArgoCD by running:
argocd login localhost:8080 --insecure

What happens during login?

  • The CLI prompts you for a username. Enter admin.
  • It then prompts for a password. Paste the password you copied from the previous command.
  • The --insecure flag is required because ArgoCD's default install uses a self-signed TLS certificate. Without it, the CLI rejects the connection.

After entering your credentials, you should see a message confirming you are logged in successfully.

Skip manual port-forwarding

If you prefer not to keep a separate terminal open for the port-forward, you can set this environment variable instead:

export ARGOCD_OPTS='--port-forward-namespace argocd'

This tells the ArgoCD CLI to handle port-forwarding automatically on every command. The rest of this project assumes the manual port-forward approach, but either method works.

Login failed or connection refused?

  • Confirm the port-forward is still running in your other terminal. If it exited, re-run the kubectl port-forward command.
  • If the password is rejected, re-run argocd admin initial-password -n argocd and copy the first line carefully. Trailing whitespace can cause failures.
  • If you see a certificate error, make sure you included the --insecure flag in the login command.
  • help me troubleshoot my ArgoCD login failure on localhost:8080

Your ArgoCD server is running and you are logged in. Next up, you will install the Sealed Secrets controller so you can safely commit encrypted credentials to Git.

Install Sealed Secrets Controller

With ArgoCD managing your deployments, the next challenge is secrets. Credentials like database passwords cannot live in plain text inside a Git repository.

Sealed Secrets solves this by encrypting secrets into a format that only your cluster can decrypt. You install a controller that holds the private key, and a CLI tool called kubeseal that encrypts secrets using the controller's public key. The encrypted output is safe to commit to Git.

In this step, get ready to:
  • Deploy the Sealed Secrets controller to your cluster.
  • Install the kubeseal CLI on your machine.
  • Verify the controller is running and fetch its public certificate.
Deploy the Sealed Secrets controller

The Sealed Secrets controller runs inside your cluster and handles decryption. When you apply a SealedSecret resource, the controller decrypts it into a standard Kubernetes Secret that your pods can use.

  • Deploy the Sealed Secrets controller to the kube-system namespace by running this command:
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.36.6/controller.yaml

What does this command do?

This applies the official Sealed Secrets controller manifest to your cluster. It creates the controller deployment, service, RBAC roles, and the SealedSecret custom resource definition (CRD) in the kube-system namespace.

On startup, the controller generates an RSA key pair. The private key stays inside the cluster as a Secret. The public key (certificate) is what you use to encrypt secrets before committing them to Git.

  • Wait for the controller pod to start by running:
kubectl get pods -n kube-system -l name=sealed-secrets-controller
  • You should see a pod with a name starting with sealed-secrets-controller in a Running state. If the status shows ContainerCreating, wait a few seconds and run the command again.

Pod not reaching Running state?

Make sure your Rancher Desktop cluster is running. Check that you can reach the cluster with kubectl get nodes. If the pod is stuck in ImagePullBackOff, your machine may not have internet access to pull the container image.

help me troubleshoot my Sealed Secrets controller pod not starting in kube-system namespace

Install the kubeseal CLI

The kubeseal CLI is the client-side tool you run on your machine. It takes a standard Kubernetes Secret and encrypts it using the controller's public certificate, producing a SealedSecret that is safe to store in Git.

  • Download, extract, and install the kubeseal binary by running these commands:
curl -OL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.36.6/kubeseal-0.36.6-linux-amd64.tar.gz"
tar -xvzf kubeseal-0.36.6-linux-amd64.tar.gz kubeseal
sudo install -m 755 kubeseal /usr/local/bin/kubeseal

What do these commands do?

  • curl -OL downloads the tarball from the official Sealed Secrets GitHub release (v0.36.6).
  • tar -xvzf extracts the kubeseal binary from the archive.
  • sudo install -m 755 moves the binary to /usr/local/bin/ with executable permissions so you can run it from anywhere.
  • Verify the installation succeeded by running:
kubeseal --version
  • You should see a version string confirming kubeseal is installed.
  • Clean up the downloaded archive by running:
rm kubeseal-0.36.6-linux-amd64.tar.gz

Permission denied or command not found?

If you see a permission error on the sudo install command, make sure you have sudo access on your machine. If kubeseal --version returns command not found, confirm the binary was placed in /usr/local/bin/ by running ls /usr/local/bin/kubeseal.

help me troubleshoot my kubeseal CLI installation on Linux

Fetch the controller's public certificate

To confirm the controller is healthy and reachable, you can ask kubeseal to fetch the controller's public certificate. This certificate is what kubeseal uses to encrypt secrets.

  • Fetch the public certificate from the controller by running:
kubeseal --fetch-cert

What does this command do?

kubeseal --fetch-cert connects to the Sealed Secrets controller through the Kubernetes API and retrieves its public certificate. By default, it looks for a controller named sealed-secrets-controller in the kube-system namespace. Since you installed the controller using the plain manifest, the naming matches automatically.

  • You should see a PEM-formatted certificate printed to your terminal, starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----.

Cannot fetch certificate?

If you see an error mentioning that the service cannot be reached, make sure the controller pod is in a Running state. Run kubectl get pods -n kube-system -l name=sealed-secrets-controller and confirm the pod is healthy. The controller needs a few seconds after starting to generate its key pair.

help me troubleshoot kubeseal --fetch-cert failing to connect to the Sealed Secrets controller

Why use the plain manifest instead of Helm?

The Helm chart installs the controller with the name sealed-secrets, but the kubeseal CLI defaults to looking for sealed-secrets-controller. Using the plain manifest avoids this naming mismatch. Everything works with default flags and no extra configuration.

Your cluster now has a working secret encryption system. Next up, you will create the GitOps repository structure that ArgoCD will watch and deploy from.

Create the GitOps Repository Structure

Your cluster now has both ArgoCD and Sealed Secrets running. The next step is to create the Git repository that ArgoCD will watch.

In a GitOps workflow, your Git repository is the single source of truth for everything running in the cluster. You need a clean directory structure that separates reusable base manifests from environment-specific customizations. Kustomize provides this through its base and overlay pattern.

In this step, get ready to:
  • Create a GitHub repository with a proper GitOps directory layout.
  • Write Kustomize base manifests for a sample Nginx deployment.
  • Create a dev overlay with environment-specific patches.
Create the GitHub repository and directory layout

ArgoCD needs a remote Git repository to watch. You will create a new public repository on GitHub and then set up the standard Kustomize directory structure inside it.

  • Open your browser and go to https://github.com/new.
  • Type gitops-demo in the Repository name field.
  • Select Public for the repository visibility.
  • Check the Add a README file option.
  • Click Create repository.

Why a public repository?

ArgoCD can connect to private repositories, but a public repo avoids the need to configure SSH keys or access tokens for this project. This keeps the focus on the GitOps workflow itself.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone your new repository by running this command:
git clone [[REPO_URL="https://github.com/YOUR-USERNAME/gitops-demo.git"]]
  • Move into the repository and create the GitOps directory structure by running these commands:
cd gitops-demo
mkdir -p base overlays/dev overlays/prod

What does this structure do?

The base/ directory holds your reusable Kubernetes manifests that are common to all environments.

The overlays/ directory contains per-environment customizations (like dev or prod) that patch the base resources without duplicating them.

  • Confirm the directories exist by running:
ls -R

You should see base, overlays/dev, and overlays/prod listed in the output.

Don't see the directories?

Make sure you ran cd gitops-demo before creating the directories. If you are still in ~/Desktop, the folders were created in the wrong location.

Help me fix my directory structure

Write the Kustomize base manifests

The base directory defines the core Kubernetes resources for a sample Nginx web application. These manifests describe the desired state that ArgoCD will deploy to your cluster.

  • Create the Deployment manifest by running this command:
cat <<EOF > base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.25.0
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
EOF

What does this manifest do?

This creates a Deployment named nginx-app that runs one replica of nginx:1.25.0. It includes resource requests and limits so the cluster knows how much CPU and memory to allocate.

The selector.matchLabels field tells the Deployment which pods belong to it. The label app: nginx-app ties the Deployment, its pods, and the Service together.

  • Create the Service manifest by running this command:
cat <<EOF > base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-app
spec:
  selector:
    app: nginx-app
  ports:
    - port: 80
      targetPort: 80
EOF

What does this manifest do?

This Service exposes port 80 on a stable cluster IP, routing traffic to any pod with the label app: nginx-app. Other services inside the cluster can reach your Nginx app using the name nginx-app.

  • Create the ConfigMap manifest by running this command:
cat <<EOF > base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-app-config
data:
  ENV: base
EOF

What does this manifest do?

This ConfigMap stores a single key-value pair indicating the environment. It is a placeholder that overlays can override for each environment (dev, prod, etc.).

  • Create the Kustomization file that ties all base resources together by running this command:
cat <<EOF > base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - configmap.yaml
EOF

What does this file do?

The kustomization.yaml file is the entry point for Kustomize. It lists all the resource files that belong to this base. When ArgoCD processes this directory, it reads this file to know which manifests to include.

  • Verify the base files exist by running:
ls base/

You should see configmap.yaml, deployment.yaml, kustomization.yaml, and service.yaml listed.

Missing a file?

If any file is missing, re-run the corresponding cat <<EOF command. Make sure the path starts with base/ and that you are in the gitops-demo directory when you run it.

Help me find my missing base file

✔️ Awesome, I've got everything!

Great. Make sure all four files are present in base/ before moving on.

ⓧ I'd like to double check the full code

Here are all four base files for comparison:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
        - name: nginx
          image: nginx:1.25.0
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
apiVersion: v1
kind: Service
metadata:
  name: nginx-app
spec:
  selector:
    app: nginx-app
  ports:
    - port: 80
      targetPort: 80
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-app-config
data:
  ENV: base
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - configmap.yaml
Create the dev overlay with environment-specific patches

Overlays let you customize the base for a specific environment without editing the base files directly. The dev overlay will increase the replica count and adjust resource limits for the development environment.

  • Create the dev overlay kustomization file by running this command:
cat <<EOF > overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
  - ../../base
patches:
  - path: replica-patch.yaml
EOF

What does this overlay do?

This kustomization references the base directory with ../../base and sets namespace: dev so all resources deploy into a dedicated dev namespace.

It also applies a patch file to override the replica count and resource limits.

  • Create the replica patch file by running this command:
cat <<EOF > overlays/dev/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: nginx
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 256Mi
EOF

What does this patch do?

This is a strategic merge patch. It targets the Deployment named nginx-app and overrides specific fields: it bumps replicas from 1 to 2 and increases the CPU and memory limits. Every other field from the base stays unchanged.

  • Verify both overlay files exist by running:
ls overlays/dev/

You should see kustomization.yaml and replica-patch.yaml listed.

Only seeing one file?

Re-run the missing cat <<EOF command. Ensure the output path starts with overlays/dev/ and that your terminal is in the gitops-demo root directory.

Help me find my missing overlay file

✔️ Awesome, I've got everything!

Great. You now have a complete base and dev overlay ready to push.

ⓧ I'd like to double check the full code

Here are both dev overlay files for comparison:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
  - ../../base
patches:
  - path: replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: nginx
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 200m
              memory: 256Mi
  • Commit all your files and push to GitHub by running these commands:
git add .
git commit -m "Add GitOps base and dev overlay"
git push origin main

What does this do?

This stages all new files, commits them with a descriptive message, and pushes to the main branch on GitHub. Once pushed, your repository becomes the source of truth that ArgoCD will reconcile against.

  • Open your browser and navigate to your gitops-demo repository on GitHub.

You should see the base/ and overlays/ directories listed in the repository file tree.

Push rejected or failed?

If git push fails with an authentication error, make sure your Git credentials (SSH key or personal access token) are configured. You may need to run git remote -v to check your remote URL matches your GitHub account.

Help me fix my Git push

Your GitOps repository is ready. Next up, you will connect ArgoCD to this repository and watch it deploy your app to the cluster for the first time.

Connect ArgoCD to Your Repository

Your GitOps repository is on GitHub with a proper Kustomize structure, and ArgoCD is running in your cluster. Now you need to connect the two so ArgoCD knows where to look for your desired cluster state.

In this step, you will register your repository with ArgoCD, define a declarative Application resource, and trigger the first sync. Once complete, ArgoCD deploys your nginx app automatically from Git.

In this step, get ready to:
  • Register your GitHub repository with ArgoCD.
  • Create a declarative ArgoCD Application manifest.
  • Sync your app to the cluster and verify it in the ArgoCD UI.
Register your GitHub repository with ArgoCD

ArgoCD needs to know about your Git repository before it can pull manifests from it. Registering the repo makes it visible in ArgoCD's repository list and confirms connectivity.

  • Register your public GitHub repository with ArgoCD by running this command:
argocd repo add https://github.com/[[GITHUB_USERNAME="your-github-username"]]/gitops-demo

What does this command do?

The argocd repo add command registers a Git repository connection with ArgoCD. For a public repository, no authentication flags are needed. ArgoCD confirms it can reach the repo and adds it to its managed repository list.

You should see output confirming the repository was added successfully.

  • Verify the registration by listing ArgoCD's known repositories:
argocd repo list

You should see your gitops-demo repository in the output with a successful connection status.

Repository connection failed?

Make sure your repository URL matches exactly what you see on GitHub. The URL must be the HTTPS clone URL. Also confirm your ArgoCD CLI session is still active by running argocd account get-user-info.

help me troubleshoot why ArgoCD cannot connect to my GitHub repository

Create a declarative ArgoCD Application manifest

ArgoCD uses an Application custom resource to define which repository path to watch, which cluster to deploy to, and which namespace to target. Defining this as a YAML file means you can version-control the application definition itself.

  • Create the dev namespace in your cluster so ArgoCD has a target to deploy into:
kubectl create namespace dev

You should see a message confirming the namespace was created.

  • Confirm the namespace exists by running:
kubectl get namespace dev

You should see the dev namespace listed with status Active.

Namespace already exists?

If you see a message that the namespace already exists, that is fine. You can continue to the next instruction.

help me understand why kubectl says namespace dev already exists

  • Create a file called gitops-demo-app.yaml in the root of your local gitops-demo repository. Paste the following content:
# ArgoCD Application definition - tells ArgoCD what to deploy and where
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: gitops-demo
  namespace: argocd
spec:
  # Use the default ArgoCD project
  project: default
  source:
    # Your GitHub repository URL
    repoURL: https://github.com/[[GITHUB_USERNAME="your-github-username"]]/gitops-demo
    # Track the main branch
    targetRevision: main
    # Render manifests from the dev overlay
    path: overlays/dev
  destination:
    # Deploy to the same cluster ArgoCD runs in
    server: https://kubernetes.default.svc
    namespace: dev
  # Manual sync for now - you will enable auto-sync in the next step
  syncPolicy: {}

What does this manifest do?

  • apiVersion: argoproj.io/v1alpha1 declares this as an ArgoCD Application custom resource.
  • source.repoURL points ArgoCD to your GitHub repository.
  • source.path: overlays/dev tells ArgoCD to render manifests from your dev Kustomize overlay.
  • destination.server: https://kubernetes.default.svc targets the same cluster ArgoCD runs in. No external cluster registration needed.
  • syncPolicy: {} means manual sync for now. You will enable auto-sync in the next step.

Why manual sync first?

Starting with manual sync lets you observe exactly what ArgoCD does during reconciliation. You can inspect the diff, confirm the resources match your expectations, and then safely enable automated sync afterwards.

  • Save gitops-demo-app.yaml.
Apply the Application and watch ArgoCD sync

With the manifest ready, you can apply it to your cluster. ArgoCD detects the new Application resource and compares your Git repo state against the cluster.

  • Apply the Application manifest to your cluster by running:
kubectl apply -f gitops-demo-app.yaml

Why kubectl apply instead of argocd app create?

Both approaches work. Using kubectl apply -f with a YAML file follows the GitOps principle of declarative, version-controllable configuration. The file itself becomes the source of truth for how the Application is defined.

  • Trigger the first sync by running:
argocd app sync gitops-demo

ArgoCD fetches your manifests from overlays/dev, renders them through Kustomize, and applies the resulting resources to the dev namespace.

  • Check the application status to confirm it synced and is healthy:
argocd app get gitops-demo

You should see the status showing Synced and Healthy. The output also lists the deployed resources: a Deployment, a Service, and a ConfigMap.

  • Verify the pods are running in the dev namespace:
kubectl get pods -n dev

You should see 2 pods (matching the replica count in your dev overlay) with status Running.

  • Open the ArgoCD UI in your browser at https://localhost:8080.
  • Click on the gitops-demo application tile.
  • You should see a visual tree showing all deployed resources with green hearts indicating a healthy state.

Application stuck on OutOfSync or Progressing?

Run argocd app get gitops-demo to check for error messages. Common causes include a typo in the repoURL or path field, or the dev namespace not existing.

If you see resources in Progressing state, wait 30 seconds. The Deployment needs time to pull the nginx:1.25.0 container image.

help me debug why my ArgoCD application is not syncing correctly

✔️ Awesome, I've got everything!

Great. Double check you have saved gitops-demo-app.yaml and that your application shows Synced and Healthy in the ArgoCD UI.

ⓧ I'd like to double check the full code

Here is the complete gitops-demo-app.yaml file:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: gitops-demo
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/<your-username>/gitops-demo
    targetRevision: main
    path: overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: dev
  syncPolicy: {}

Your ArgoCD is now watching your Git repository and has deployed the nginx app to your cluster. Next up, you will enable auto-sync and self-heal so ArgoCD automatically reverts any manual changes made directly to the cluster.

Enable Auto-Sync and Self-Heal

Right now, your ArgoCD application requires a manual sync every time you push a change to Git. If someone edits the cluster directly, ArgoCD notices the drift but waits for you to act.

In this step, you'll flip on automated sync with self-healing. ArgoCD will automatically apply Git changes and revert any unauthorized manual edits within seconds.

In this step, get ready to:
  • Enable automated sync with self-heal and pruning on the ArgoCD Application.
  • Demonstrate drift by manually scaling the deployment with kubectl.
  • Watch ArgoCD detect and automatically revert the unauthorized change.
Update the ArgoCD Application to enable automated sync

ArgoCD's sync policy controls whether syncs happen automatically or require manual triggering. By default, both selfHeal and prune are disabled.

  • Open gitops-demo-app.yaml in your editor.
  • Find this line at the bottom of the file:
  syncPolicy: {}
  • Replace that line with the following syncPolicy block, so your file ends with:
  syncPolicy:
    automated:
      selfHeal: true
      prune: true

What do selfHeal and prune do?

selfHeal: true tells ArgoCD to revert any manual cluster changes that don't match Git. It re-attempts after a 5-second timeout when it detects drift.

prune: true tells ArgoCD to delete resources from the cluster if they are removed from Git. Without this, deleting a manifest from your repo leaves the resource running.

  • Save gitops-demo-app.yaml.
  • Apply the updated Application to your cluster by running this command:
kubectl apply -f gitops-demo-app.yaml
  • Verify the sync policy is active by running:
argocd app get gitops-demo

You should see Sync Policy: Automated (Prune, Self Heal) in the output.

Still seeing Sync Policy: <none>?

Make sure you replaced the entire syncPolicy: {} line, including the curly braces. The YAML indentation must be exactly 4 spaces under syncPolicy for the automated key.

Check that you ran kubectl apply -f gitops-demo-app.yaml from the directory containing the file.

Why isn't my ArgoCD sync policy updating after applying the manifest?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: gitops-demo
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/[[GITHUB_USERNAME="your-github-username"]]/gitops-demo
    targetRevision: main
    path: overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: dev
  syncPolicy:
    automated:
      selfHeal: true
      prune: true
Demonstrate drift detection by manually scaling the deployment

Your Git repository declares 2 replicas for the nginx-app deployment (via the dev overlay patch). You'll now simulate an unauthorized change by scaling the deployment directly in the cluster.

  • Scale the deployment to 3 replicas by running this command:
kubectl scale deployment/nginx-app -n dev --replicas=3
  • Immediately check the replica count by running:
kubectl get deployment nginx-app -n dev

You should briefly see 3/3 in the READY column. This confirms your manual scale took effect.

Why is this a problem?

In a real environment, someone might SSH into a cluster and run kubectl scale to "fix" something quickly. This bypasses the PR review process and leaves no audit trail. The cluster state no longer matches what's declared in Git.

Watch ArgoCD detect the drift and automatically revert

With self-heal enabled, ArgoCD detects that the live cluster state (3 replicas) no longer matches the Git-declared state (2 replicas). It performs a partial sync to bring the cluster back in line.

  • Wait about 10 seconds, then check the deployment again by running:
kubectl get deployment nginx-app -n dev

You should see the READY column back to 2/2. ArgoCD detected the drift and reverted the replica count to match your Git repository.

  • Confirm the full application status by running:
argocd app get gitops-demo

The status should show Synced and Healthy. Your cluster is back in its declared state.

Still seeing 3 replicas after waiting?

ArgoCD's self-heal re-attempts after a 5-second timeout. If you check too quickly, the revert may not have happened yet. Wait 15 seconds and run kubectl get deployment nginx-app -n dev again.

If replicas stay at 3 after 30 seconds, check that your sync policy was applied correctly with argocd app get gitops-demo. Look for Self Heal in the Sync Policy line.

Why isn't ArgoCD reverting my manual kubectl changes?

Why self-heal matters in production

Self-healing ensures your cluster can never silently drift from what's declared in Git. Every running resource is guaranteed to match a reviewed, merged pull request. If something changes outside of Git, ArgoCD brings it back automatically.

Your cluster now self-heals. Any manual change gets reverted to match Git within seconds. Next up, you'll encrypt a secret with Sealed Secrets and commit it safely to your GitOps repository.

Manage Secrets with Sealed Secrets

Your cluster now self-heals whenever someone makes a manual change. But there's one type of resource you haven't committed to Git yet: secrets.

Kubernetes Secrets are only base64-encoded, not encrypted. Committing them directly to Git would expose credentials to anyone with repository access. Sealed Secrets solves this by encrypting secrets with a public key so that only the controller running in your cluster can decrypt them.

In this step, get ready to:
  • Create a Kubernetes Secret and encrypt it with kubeseal.
  • Commit the SealedSecret manifest to your GitOps repository.
  • Verify ArgoCD syncs and decrypts the secret in your cluster.
Create a Kubernetes Secret and encrypt it with kubeseal

The kubeseal CLI takes a regular Kubernetes Secret as input and outputs an encrypted SealedSecret. The encrypted output is safe to commit to Git because only the Sealed Secrets controller in your cluster holds the private key to decrypt it.

  • Make sure you're in your local gitops-demo repository folder.
  • Generate an encrypted SealedSecret for a database credential by running this command:
kubectl create secret generic db-credentials \
  --namespace=dev \
  --from-literal=DB_PASSWORD=supersecretpassword123 \
  --dry-run=client -o yaml | \
  kubeseal -o yaml > overlays/dev/db-credentials-sealed.yaml

What does this command do?

This is a two-part pipeline connected with | (a pipe).

  • kubectl create secret generic db-credentials generates a Secret manifest with name db-credentials in the dev namespace. The --dry-run=client -o yaml flags output the YAML without actually creating it in the cluster.
  • kubeseal -o yaml encrypts that Secret using the controller's public key and outputs a SealedSecret manifest.
  • The > overlays/dev/db-credentials-sealed.yaml redirect saves the encrypted result to your dev overlay folder.

What is strict scope?

Sealed Secrets uses strict scope by default. This means the encrypted secret is bound to the exact name (db-credentials) and namespace (dev) you specified. If someone tries to change either one, decryption will fail.

  • Verify the encrypted file was created by running:
cat overlays/dev/db-credentials-sealed.yaml
  • You should see a YAML file with kind: SealedSecret and a long encrypted string under encryptedData.DB_PASSWORD. The plaintext password does not appear anywhere in the file.

Seeing an error about the controller?

If kubeseal cannot connect to the controller, make sure the Sealed Secrets controller pod is running in kube-system. Check with kubectl get pods -n kube-system and look for a pod with sealed-secrets-controller in the name.

Why can't kubeseal encrypt my secret?

Commit the SealedSecret to your GitOps repository

The encrypted file now exists locally, but ArgoCD doesn't know about it yet. You need to register it in your Kustomize overlay and push the changes to Git.

  • Open overlays/dev/kustomization.yaml in your text editor.
  • Find the resources: section. It currently looks like this:
resources:
  - ../../base
  • Add db-credentials-sealed.yaml as a new resource entry, so the section now looks like this:
resources:
  - ../../base
  - db-credentials-sealed.yaml
  • Save overlays/dev/kustomization.yaml.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before continuing.

ⓧ I'd like to double check the full code

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
  - ../../base
  - db-credentials-sealed.yaml
patches:
  - path: replica-patch.yaml

Now commit and push both files to your repository.

  • Stage, commit, and push the SealedSecret and updated kustomization by running these commands:
git add overlays/dev/db-credentials-sealed.yaml overlays/dev/kustomization.yaml
git commit -m "Add sealed database credential to dev overlay"
git push origin main

Why is this safe to push?

The file you're pushing contains only the encrypted ciphertext. Anyone who reads db-credentials-sealed.yaml in your repository will see a long encrypted string, not the actual password. Only the Sealed Secrets controller in your cluster can decrypt it.

  • You should see the push succeed with a message showing the commit was sent to main.

Push rejected or authentication failed?

Make sure you're authenticated with GitHub. If using HTTPS, you may need a personal access token. If using SSH, confirm your key is added with ssh -T git@github.com.

Help me fix my git push

Verify ArgoCD syncs the SealedSecret

Since auto-sync is enabled, ArgoCD will detect the new commit and apply the SealedSecret to your cluster. The Sealed Secrets controller then decrypts it into a regular Kubernetes Secret that your pods can use.

  • Wait about 30 seconds for ArgoCD to detect the change, then check the application status by running:
argocd app get gitops-demo
  • You should see Sync Status: Synced and Health Status: Healthy. The resource list should now include a SealedSecret resource.
  • Confirm the controller decrypted the SealedSecret into a usable Kubernetes Secret by running:
kubectl get secret db-credentials -n dev
  • You should see a Secret named db-credentials of type Opaque in the output.
  • Verify the actual secret value was decrypted correctly by running:
kubectl get secret db-credentials -n dev -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
  • You should see supersecretpassword123 printed to your terminal. This is the original credential you encrypted earlier, now decrypted and available for pods to consume.

What just happened end to end?

  • You encrypted a credential locally with kubeseal and committed only the encrypted version to Git.
  • ArgoCD detected the new commit and applied the SealedSecret manifest to the cluster.
  • The Sealed Secrets controller recognized the SealedSecret CRD, decrypted it using its private key, and created a standard Kubernetes Secret.
  • Your application pods can now mount or reference that Secret like any normal credential. The plaintext never touched Git.

Secret not appearing in the dev namespace?

If the SealedSecret is synced but no decrypted Secret appears, check the controller logs for errors by running kubectl logs -n kube-system -l name=sealed-secrets-controller. A common cause is a mismatch between the namespace in the SealedSecret metadata and the namespace you specified during encryption.

If ArgoCD still shows OutOfSync, make sure you added db-credentials-sealed.yaml to the resources: list in overlays/dev/kustomization.yaml and pushed both files.

Help me debug my Sealed Secret decryption

Your secrets are now encrypted and managed through Git with a full audit trail. Next up, you'll automate image tag updates so that new container builds flow through pull requests before reaching your cluster.

Automate Image Updates with GitHub Actions

Your cluster now self-heals and your secrets live safely in Git. The missing piece is automation for the most common change in any deployment pipeline: updating the container image tag.

In a GitOps workflow, you never run kubectl set image directly. Instead, a CI system opens a pull request to change the image tag in your GitOps repo. ArgoCD picks up the merged change and deploys it. This gives you a full audit trail for every deployment.

In this step, get ready to:
  • Create a GitHub Actions workflow that opens a PR to update the image tag.
  • Push the workflow to your repository and trigger it manually.
  • Review the auto-generated PR showing the image tag change with a full audit trail.
Create a GitHub Actions workflow to update image tags

The workflow will accept a new image tag as input, update the Kustomize overlay file using sed, and open a pull request with the change. For sed to find and replace the tag, your kustomization.yaml needs an explicit images section.

  • Open overlays/dev/kustomization.yaml and add the following images section at the bottom of the file:
images:
  - name: nginx
    newTag: 1.25.0

What does this do?

Kustomize's images transformer overrides the image tag for any container named nginx in the base manifests. Setting newTag: 1.25.0 pins the same version you already have. The important part is that the newTag field now exists for the workflow's sed command to find and replace.

  • Save overlays/dev/kustomization.yaml.

ArgoCD showing OutOfSync after this change?

Adding an images transformer that sets the same tag should not cause drift. If ArgoCD flags it as OutOfSync, commit and push this change first so ArgoCD reconciles before you proceed.

help me understand why ArgoCD shows OutOfSync after adding a Kustomize images transformer

✔️ Awesome, I've got everything!

Great. Double check you have saved overlays/dev/kustomization.yaml before continuing.

ⓧ I'd like to double check the full code

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
  - ../../base
  - db-credentials-sealed.yaml
patches:
  - path: replica-patch.yaml
images:
  - name: nginx
    newTag: 1.25.0

Now you will create the GitHub Actions workflow file that automates image tag PRs.

  • Create the .github/workflows/ directory structure in the root of your gitops-demo repository by running:
mkdir -p .github/workflows
  • Create and open the file .github/workflows/update-image-tag.yaml and paste the following workflow:
name: Update Image Tag
on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: 'New nginx image tag'
        required: true
        default: '1.25.3'
jobs:
  update-tag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Update image tag
        run: |
          sed -i "s|newTag: .*|newTag: ${{ github.event.inputs.image_tag }}|" overlays/dev/kustomization.yaml
      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v6
        with:
          commit-message: 'chore: update nginx image tag to ${{ github.event.inputs.image_tag }}'
          title: 'chore: update nginx image tag to ${{ github.event.inputs.image_tag }}'
          branch: update-image-tag-${{ github.event.inputs.image_tag }}
          body: 'Automated image tag update via GitHub Actions.'

What does this workflow do?

  • workflow_dispatch makes the workflow manually triggerable from the GitHub UI. It accepts an image_tag input with a default of 1.25.3.
  • actions/checkout@v4 clones the repository so the workflow can edit files.
  • The sed command replaces the newTag line in your overlay's kustomization.yaml with the new tag value.
  • peter-evans/create-pull-request@v6 commits the change and opens a pull request automatically, giving you a complete audit trail.
  • Save .github/workflows/update-image-tag.yaml.
  • Confirm the file exists by running:
ls .github/workflows/

You should see update-image-tag.yaml in the output.

File not showing up?

Make sure you ran mkdir -p .github/workflows from the root of your gitops-demo repository. If you are in a subdirectory, the .github folder will be created in the wrong place.

help me figure out why my workflow file is in the wrong directory

✔️ Awesome, I've got everything!

Great. Double check you have saved the workflow file before continuing.

ⓧ I'd like to double check the full code

name: Update Image Tag
on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: 'New nginx image tag'
        required: true
        default: '1.25.3'
jobs:
  update-tag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Update image tag
        run: |
          sed -i "s|newTag: .*|newTag: ${{ github.event.inputs.image_tag }}|" overlays/dev/kustomization.yaml
      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v6
        with:
          commit-message: 'chore: update nginx image tag to ${{ github.event.inputs.image_tag }}'
          title: 'chore: update nginx image tag to ${{ github.event.inputs.image_tag }}'
          branch: update-image-tag-${{ github.event.inputs.image_tag }}
          body: 'Automated image tag update via GitHub Actions.'
Push the workflow and trigger it manually

The workflow must exist on the default branch (main) before GitHub recognizes it for manual dispatch. You also need to configure repository permissions so the workflow can create pull requests.

  • Commit and push both changes (the updated kustomization and the new workflow) by running:
git add .
git commit -m "feat: add image tag update workflow and images transformer"
git push origin main
  • Open your gitops-demo repository on GitHub in your browser.
  • Click Settings in the top navigation bar.
  • In the left sidebar, click Actions, then click General.
  • Scroll down to the Workflow permissions section.
  • Select Read and write permissions.
  • Check the box for Allow GitHub Actions to create and approve pull requests.
  • Click Save.

Why change workflow permissions?

By default, personal repositories give the GITHUB_TOKEN read-only access and do not allow workflows to create pull requests. The peter-evans/create-pull-request action needs write access to push a branch and open a PR on your behalf.

  • Now trigger the workflow. Click Actions in the top navigation bar of your repository.
  • In the left sidebar, click Update Image Tag (your workflow name).
  • Click the Run workflow button.
  • Leave the branch as main and leave the image tag input as the default 1.25.3.
  • Click the green Run workflow button to start the run.

The workflow run appears in the list. Click it to watch the progress. It typically completes in under a minute.

Don't see the Run workflow button?

The Run workflow button only appears for workflows that use the workflow_dispatch trigger and are on the default branch. Double-check that your push to main succeeded and that the file is at .github/workflows/update-image-tag.yaml.

help me figure out why the Run workflow button is not appearing in GitHub Actions

Review the auto-generated pull request

Once the workflow run succeeds, the peter-evans/create-pull-request action has already created a PR for you. This is the GitOps audit trail in action.

  • Click Pull requests in your repository's top navigation.
  • Open the PR titled chore: update nginx image tag to 1.25.3.
  • Click the Files changed tab to see the diff.

You should see a single-line change in overlays/dev/kustomization.yaml where newTag: 1.25.0 has been replaced with newTag: 1.25.3.

Why is this better than kubectl set image?

Every deployment change now has a PR with a diff, author, timestamp, and approval history. If something breaks after a deploy, you can trace the exact commit that caused it. You can also require code review on these PRs before they merge, adding a human gate to automated updates.

If you merge this PR, ArgoCD detects the change on main and automatically syncs the new image tag to your cluster.

No PR was created?

Check the workflow run logs by clicking into the failed (or successful but empty) run under Actions. Common causes:

  • You did not enable Read and write permissions or Allow GitHub Actions to create and approve pull requests in Settings > Actions > General.
  • The overlays/dev/kustomization.yaml file does not contain a newTag: line for sed to match. Make sure you added the images section in the first part of this step.

help me figure out why my GitHub Actions workflow did not create a pull request

Your image updates now flow through pull requests with a full audit trail. Next up, you will configure Renovate Bot to automatically detect when new container image versions are available and open PRs for those too.

Configure Renovate Bot for Dependency Updates

In the previous step, you automated image tag updates using a GitHub Actions workflow you trigger manually. That covers YOUR releases.

But what about third-party dependencies like the nginx base image itself? When nginx publishes a new patch release, nobody on your team might notice for weeks. Renovate Bot solves this by continuously scanning your repository for outdated container images and opening PRs automatically.

In this step, get ready to:
  • Install the Mend Renovate GitHub App on your repository.
  • Add a custom renovate.json configuration that targets your Kubernetes YAML files.
  • Merge the onboarding PR and observe Renovate opening dependency update PRs.
Install the Mend Renovate GitHub App

Renovate runs as a hosted GitHub App maintained by Mend. You install it on your repository and it handles the scanning, version lookups, and PR creation automatically.

  • Open your browser and navigate to:
  • https://github.com/apps/renovate
  • Click Install.
  • Select Only select repositories.
  • Choose your gitops-demo repository from the dropdown.
  • Click Install to confirm.

What happens after installing?

Within a few minutes, Renovate will scan your repository. Because no configuration file exists yet, it creates a "Configure Renovate" pull request on a branch called renovate/configure.

Renovate will not open any dependency update PRs until it considers the repository "onboarded". You can either merge this onboarding PR or commit your own config to the default branch.

Give it a few minutes

The Mend Renovate App processes repositories on a schedule. It may take 5 to 15 minutes for the onboarding PR to appear after installation.

Add the Renovate configuration file

The default onboarding PR proposes a minimal config. For a GitOps repository, you need to explicitly tell Renovate which files to scan. The kubernetes manager has no default file patterns, so without a custom config, Renovate would skip your YAML manifests entirely.

  • In your local gitops-demo repository, create a file called renovate.json in the repository root with the following content:
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "kubernetes": {
    "fileMatch": ["(^|/)overlays/.+\\.yaml$", "(^|/)base/.+\\.yaml$"]
  },
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "automerge": false,
      "labels": ["renovate", "dependencies"]
    }
  ]
}

What does this configuration do?

  • $schema enables autocompletion and validation in your editor.
  • extends: ["config:base"] inherits Renovate's recommended defaults (dependency dashboard, semantic commit prefixes, monorepo grouping).
  • kubernetes.fileMatch tells the kubernetes manager to scan all YAML files under overlays/ and base/. Without this, Renovate skips your manifests because the kubernetes manager has no default file patterns.
  • packageRules disables automerge for Docker image updates and adds renovate and dependencies labels to the PRs for easy filtering.
  • Commit and push the configuration file by running these commands:
git add renovate.json
git commit -m "chore: add renovate configuration for kubernetes"
git push origin main
  • Confirm the push succeeded by checking for the commit hash in the terminal output.

Push rejected?

If you see a rejection error, pull first with git pull --rebase origin main then push again. This can happen if Renovate's onboarding PR branch triggered a merge commit on main.

help me fix a git push rejection on my gitops-demo repository

Merge the onboarding PR and observe dependency updates

Now that your custom config exists on the main branch, Renovate recognizes the repository as onboarded. If an onboarding PR is still open, you can close it since the config is already in place.

  • Open your gitops-demo repository on GitHub.
  • Click the Pull requests tab.
  • If you see a PR titled "Configure Renovate", click into it and click Close pull request. Your config is already on main, so this PR is no longer needed.
  • Wait 5 to 15 minutes for Renovate to run its next scan.

What will Renovate find?

Renovate's kubernetes manager will scan your YAML files and detect the nginx:1.25.0 image reference in base/deployment.yaml and the newTag: 1.25.0 in overlays/dev/kustomization.yaml.

If a newer nginx image exists (e.g. 1.27.x), Renovate opens a PR proposing the update with the renovate and dependencies labels attached.

  • After waiting, refresh the Pull requests tab. You should see one or more PRs opened by Renovate proposing container image version updates.
  • Click into one of the Renovate PRs to inspect the proposed change. You should see a diff updating the image tag in your Kustomize files, along with release notes and changelog information.

Why is this important for GitOps?

Every dependency update flows through a pull request, giving you a clear audit trail. You review the change, merge it, and ArgoCD syncs the new image tag to your cluster automatically.

No one manually edits cluster resources. No one forgets to update a base image for months. The entire lifecycle is Git-driven.

No PRs appearing after 15 minutes?

  • Verify that renovate.json exists on the main branch by checking GitHub's file list.
  • Confirm the Mend Renovate App is installed by going to your repository's Settings > Integrations > GitHub Apps.
  • Check that the "Configure Renovate" onboarding PR is closed or absent (if it's still open and unmerged, Renovate won't scan).
  • Look for a Dependency Dashboard issue in your repository's Issues tab. If it exists, Renovate is active.

help me troubleshoot why Renovate Bot is not opening PRs on my gitops-demo repository

✔️ Awesome, I've got everything!

Great. Make sure you've saved all files and your renovate.json is committed to main.

ⓧ I'd like to double check the full code

Here is the complete renovate.json file:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "kubernetes": {
    "fileMatch": ["(^|/)overlays/.+\\.yaml$", "(^|/)base/.+\\.yaml$"]
  },
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "automerge": false,
      "labels": ["renovate", "dependencies"]
    }
  ]
}

Your GitOps pipeline is now fully automated end-to-end. Image tags get bumped via GitHub Actions, third-party dependencies get tracked by Renovate, and every change lands via a PR that ArgoCD syncs to your cluster. Next up, the Secret Mission adds production-grade deployment hooks and notifications.

Secret mission

Add PreSync Hooks and GitHub Commit Status Notifications

Your GitOps pipeline already ensures every deployment goes through Git. But production pipelines need more. What if a database migration must run before the new code deploys? What if your team needs real-time deployment status on every commit in GitHub? In this secret mission, you will add a PreSync hook that simulates a database migration job and configure ArgoCD Notifications to post deployment status to GitHub commit statuses.

Clean Up Your Resources

Clean Up Your Resources

All resources in this project run locally on your Rancher Desktop k3s cluster. No cloud resources were created, so there are no ongoing costs.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • ArgoCD deployment and namespace (argocd namespace).
  • Sealed Secrets controller (kube-system namespace).
  • Application namespace (dev).
  • ArgoCD CLI binary (/usr/local/bin/argocd).
  • kubeseal CLI binary (/usr/local/bin/kubeseal).
  • ArgoCD config cache (~/.config/argocd).
  • Local Git clone of your GitOps repository.
  • Renovate Bot GitHub App (installed on your repository).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your GitOps pipeline.

  • ArgoCD will continue reconciling your cluster state against your Git repository.
  • The Sealed Secrets controller will continue decrypting any SealedSecrets in the cluster.
  • Renovate Bot will continue opening PRs for dependency updates on your GitHub repository.

Pause - I'll come back to this later

Shut down running processes to free up memory while keeping all your files and configuration intact.

  • Scale down all ArgoCD deployments by running:
kubectl -n argocd scale deployment --all --replicas=0
  • This stops ArgoCD from consuming resources while preserving all configuration.
  • When you're ready to resume, scale back up:
kubectl -n argocd scale deployment --all --replicas=1
  • Your Git repository, CLI binaries, and Renovate Bot configuration all remain untouched.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps in order to fully clean up.

Delete the ArgoCD application and namespace
  • Delete the ArgoCD application (this also removes all managed resources from the cluster):
argocd app delete gitops-demo --cascade
  • Delete the ArgoCD namespace:
kubectl delete namespace argocd
Delete the Sealed Secrets controller and CRD
  • Remove the Sealed Secrets controller:
kubectl delete -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.36.6/controller.yaml
  • Remove the SealedSecret custom resource definition:
kubectl delete crd sealedsecrets.bitnami.com
Delete the application namespace
  • Remove the dev namespace and all resources inside it:
kubectl delete namespace dev
Remove CLI binaries
  • Remove the ArgoCD and kubeseal CLI binaries:
sudo rm /usr/local/bin/argocd
sudo rm /usr/local/bin/kubeseal
Clean up config and temp files
  • Remove the ArgoCD CLI config cache:
rm -rf ~/.config/argocd
  • Remove any downloaded tarballs and temp files that remain in your working directory:
rm -f argocd-linux-amd64 kubeseal-0.36.6-linux-amd64.tar.gz kubeseal
Remove the local Git clone
  • Delete your local clone of the GitOps repository (replace the path with your actual directory):
rm -rf [[GITOPS_REPO_PATH="path to your local gitops-repo folder"]]
Uninstall Renovate Bot
Optional: reclaim cached container images

Your cluster cached several container images during this project. To reclaim that disk space, open Rancher Desktop and use the Reset Kubernetes option in preferences.

After these steps, your system is back to its pre-project state with no leftover caches, binaries, or cluster resources.

That's a Wrap!

That's a Wrap!

Nice work. You just built a complete GitOps deployment pipeline from scratch.

Your Git repository is now the single source of truth for everything running in your cluster. Every change flows through a pull request, giving you a full audit trail from code to production.

Here's what you accomplished:

  • Installed ArgoCD as your cluster's continuous delivery engine with auto-sync and self-heal, ensuring your cluster always matches Git and reverts any manual drift.
  • Encrypted credentials with Sealed Secrets and committed them safely to your GitOps repository, structured with Kustomize base and environment overlays.
  • Automated deployments through PR-based workflows using GitHub Actions for image tag updates and Renovate Bot for dependency updates.
  • Added PreSync hooks for database migrations and configured ArgoCD notifications to post deployment status to GitHub commit statuses.

Ready to quiz yourself?