Kubernetes Cluster Setup

Before we can explore GitOps, we need a Kubernetes cluster to work with. While cloud providers offer managed Kubernetes services, they cost money and add unnecessary complexity when learning. Instead, we’ll use minikube - a tool that runs a complete Kubernetes cluster on your laptop, completely free.

By the end of this tutorial, you’ll have:

  • A working local Kubernetes cluster
  • All necessary tools installed and configured
  • A test application running to verify everything works
  • GitHub repository ready
  • ArgoCD installed and ready to deploy

This foundation enables you to learn production-grade GitOps patterns without infrastructure costs or complexity. Everything you learn here applies directly to production Kubernetes clusters.

Step 1: Choose Your minikube Driver

minikube needs a way to create virtual machines or containers to run Kubernetes. You have several options:

A driver is the technology minikube uses to create the environment where Kubernetes runs. Think of it as the foundation:

  • Container-based (Docker, Podman) - Runs Kubernetes in a container
  • VM-based (VirtualBox, Hyper-V, KVM) - Runs Kubernetes in a virtual machine

Tip

For this series, we recommend Docker. It’s the most common setup and has the best community support. All examples will use Docker, but everything works with other drivers too. More documentation about minikube drivers can be found here.

Step 2: Install kubectl

kubectl is the command-line tool for interacting with Kubernetes clusters. You’ll use it constantly, regardless of which driver you choose.

Tip

In this series, we will use Homebrew to install kubectl, but feel free to use the method that you prefer. More documentation about kubectl installation can be found here.

# Using Homebrew (recommended)
brew install kubectl

# Verify installation
kubectl version --client

Expected output:

Client Version: v1.32.x
Kustomize Version: v5.5.x

Tip

The kubectl version doesn’t need to exactly match your cluster version. kubectl supports clusters within one minor version (older or newer).

Step 3: Install Your Driver

Tip

In this series, we will use Docker, but feel free to use the driver that you prefer. More documentation about minikube drivers can be found here.

Check if you already have Docker:

docker --version

If that works, skip to Step 4! Otherwise, install Docker:

For Windows, and MacOS, you can install Docker from the Docker website.

For Linux, you can install the Docker Engine using the recommended methods for your distribution.

Step 4: Install minikube

To install minikube, you can use the recommended methods for your distribution.

# Verify installation
minikube version

Expected output:

minikube version: v1.37.x
commit: xxxxxxxxxxxxx

Step 5: Start Your First Kubernetes Cluster

Now for the exciting part - starting your cluster!

Start minikube (With Docker)

# Start cluster with Docker driver
minikube start --driver=docker --cpus=2 --memory=4096

# This will take 2-5 minutes on first run
# minikube downloads the Kubernetes components

Start minikube (With Other Drivers)

# Start cluster with VirtualBox driver
minikube start --driver=virtualbox --cpus=2 --memory=4096

# Hyper-V (Windows)
minikube start --driver=hyperv --cpus=2 --memory=4096

# KVM (Linux)
minikube start --driver=kvm2 --cpus=2 --memory=4096

# Let minikube choose automatically
minikube start --cpus=2 --memory=4096

Expected output:

😄  minikube v1.37.0 on Darwin 15.6.1 (arm64)
✨  Using the docker driver based on user configuration
📌  Using Docker Desktop driver with root privileges
👍  Starting "minikube" primary control-plane node in "minikube" cluster
🚜  Pulling base image v0.0.48 ...
💾  Downloading Kubernetes v1.34.0 preload ...
    > preloaded-images-k8s-v18-v1...:  332.38 MiB / 332.38 MiB  100.00% 73.20 M
    > gcr.io/k8s-minikube/kicbase...:  450.06 MiB / 450.06 MiB  100.00% 48.43 M
🔥  Creating docker container (CPUs=2, Memory=4096MB) ...
🐳  Preparing Kubernetes v1.34.0 on Docker 28.4.0 ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: default-storageclass, storage-provisioner
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Tip

If you have more resources available, you can increase CPUs and memory: minikube start --cpus=4 --memory=8192

Note

The first start takes longer because minikube downloads the Kubernetes container images. Subsequent starts are much faster.

Verify Cluster Health

# Check cluster status
minikube status

Expected output:

minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured
# Check kubectl connection
kubectl cluster-info

Expected output:

Kubernetes control plane is running at https://127.0.0.1:60737
CoreDNS is running at https://127.0.0.1:60737/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
# View cluster nodes
kubectl get nodes

Expected output:

NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   3m    v1.34.0

Access Kubernetes Dashboard

minikube includes a web-based dashboard for visualizing your cluster:

# Start the dashboard (opens in browser)
minikube dashboard

This opens a browser window showing:

  • Cluster resource usage
  • Running workloads
  • Deployments, pods, services
  • Logs and events

Tip

Keep the dashboard open in a browser tab - it’s helpful for visualizing what’s happening as you learn GitOps.

Step 6: Deploy a Test Application

Let’s verify everything works by deploying a simple application.

Create a Test Deployment

# Deploy nginx web server
kubectl create deployment nginx --image=nginx:latest

# Verify deployment created
kubectl get deployments

Expected output:

NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   1/1     1            1           10s
# Check the pod
kubectl get pods

Expected output:

NAME                     READY   STATUS    RESTARTS   AGE
nginx-xxxxxxxxxx-xxxxx   1/1     Running   0          20s

Expose the Application

# Create a service to access nginx
kubectl expose deployment nginx --type=NodePort --port=80

# Get the service URL
minikube service nginx --url

Expected output:

http://127.0.0.1:xxxxx

Open that URL in your browser - you should see the “Welcome to nginx!” page.

View Application Logs

# Get logs from the nginx pod
kubectl logs deployment/nginx

# Follow logs in real-time
kubectl logs deployment/nginx -f

Expected output:

/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/10/06 23:41:09 [notice] 1#1: using the "epoll" event method
2025/10/06 23:41:09 [notice] 1#1: nginx/1.29.1
2025/10/06 23:41:09 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14+deb12u1)
2025/10/06 23:41:09 [notice] 1#1: OS: Linux 6.10.14-linuxkit
2025/10/06 23:41:09 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2025/10/06 23:41:09 [notice] 1#1: start worker processes
2025/10/06 23:41:09 [notice] 1#1: start worker process 29
...

Clean Up Test Resources

# Delete the test deployment and service
kubectl delete deployment nginx
kubectl delete service nginx

# Verify cleanup
kubectl get deployments,services

Tip

In the future, we won’t use kubectl create or kubectl delete commands. Everything will be managed through Git and ArgoCD!

Step 7: Enable Ingress Controller

An ingress controller manages external access to services in your cluster, typically HTTP/HTTPS. minikube includes an NGINX ingress controller addon that we’ll use to access ArgoCD via a URL instead of port-forwarding.

# Enable the ingress addon
minikube addons enable ingress

# Wait for the ingress controller to be ready (takes 30-60 seconds)
kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=120s

Expected output:

💡  ingress is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
You can view the list of minikube maintainers at: https://github.com/kubernetes/minikube/blob/master/OWNERS
💡  After the addon is enabled, please run "minikube tunnel" and your ingress resources would be available at "127.0.0.1"
    ▪ Using image registry.k8s.io/ingress-nginx/controller:v1.13.2
    ▪ Using image registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.6.2
    ▪ Using image registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.6.2
🔎  Verifying ingress addon...
🌟  The 'ingress' addon is enabled
pod/ingress-nginx-controller-xxxxxxxxxx-xxxxx condition met

Verify Ingress Controller

# Check the ingress controller pod
kubectl get pods -n ingress-nginx

Expected output:

NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-xxxxx        0/1     Completed   0          60s
ingress-nginx-admission-patch-xxxxx         0/1     Completed   0          60s
ingress-nginx-controller-xxxxxxxxxx-xxxxx   1/1     Running     0          60s

Note

The ingress controller is now ready to route external traffic to services in your cluster. We’ll use this for ArgoCD in the next section.

Note

After the addon is enabled, please run minikube tunnel and your ingress resources would be available at 127.0.0.1.

For more information about using ingress with minikube, see the Kubernetes documentation.

ArgoCD Installation & First Deployment

Now that we have a working Kubernetes cluster, let’s install ArgoCD and experience GitOps in action.

Info

If you haven’t read the series introduction, start there to understand the GitOps principles we’re implementing. This section focuses on the practical setup.

Step 1: Install ArgoCD

ArgoCD is a GitOps operator that continuously monitors Git repositories and keeps your cluster in sync.

Install ArgoCD in Your Cluster

For more information about ArgoCD, you can check the official documentation and the Getting Started Guide.

# Create namespace for ArgoCD
kubectl create namespace argocd

# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# This installs all ArgoCD components
# Wait for pods to be ready (takes 1-2 minutes)
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=300s

Expected output:

namespace/argocd created
...
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
serviceaccount/argocd-application-controller created
serviceaccount/argocd-applicationset-controller created
serviceaccount/argocd-dex-server created
serviceaccount/argocd-notifications-controller created
serviceaccount/argocd-redis created
serviceaccount/argocd-repo-server created
serviceaccount/argocd-server created
...
pod/argocd-server-xxxxxxxxxx-xxxxx condition met

Verify ArgoCD Installation

# Check all ArgoCD pods are running
kubectl get pods -n argocd

Expected output:

NAME                                                READY   STATUS    RESTARTS   AGE
argocd-application-controller-0                     1/1     Running   0          2m
argocd-applicationset-controller-xxxxxxxxxx-xxxxx   1/1     Running   0          2m
argocd-dex-server-xxxxxxxxxx-xxxxx                  1/1     Running   0          2m
argocd-notifications-controller-xxxxxxxxxx-xxxxx    1/1     Running   0          2m
argocd-redis-xxxxxxxxxx-xxxxx                       1/1     Running   0          2m
argocd-repo-server-xxxxxxxxxx-xxxxx                 1/1     Running   0          2m
argocd-server-xxxxxxxxxx-xxxxx                      1/1     Running   0          2m

Note

All pods should show 1/1 under READY and Running under STATUS. If any show 0/1 or Pending, wait another minute and check again.

(Optional) Install ArgoCD CLI

Tip

We will use Homebrew to install ArgoCD CLI, but feel free to use the method that you prefer. More documentation about ArgoCD CLI installation can be found here.

# Install ArgoCD CLI
brew install argocd

Step 2: Access ArgoCD UI

ArgoCD has a web interface for managing deployments. You can access it either via port-forwarding or using an ingress (recommended).

Get Initial Admin Password

Using the CLI

# Retrieve the auto-generated admin password
argocd admin initial-password -n argocd

Using kubectl

# Retrieve the auto-generated admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo

Expected output:

a3B9mK7sL2nP8qR4t  # Your password will be different

Tip

Save this password - you’ll need it in a moment. The username is admin.

Access ArgoCD UI

Using ingress provides a cleaner URL-based access pattern similar to production environments.

# Create an ingress resource for ArgoCD
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
  ingressClassName: nginx
  rules:
  - host: argocd.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: argocd-server
            port:
              name: https
EOF

Expected output:

ingress.networking.k8s.io/argocd-server-ingress created

Verify the ingress is created:

kubectl get ingress -n argocd

Expected output:

NAME                    CLASS   HOSTS           ADDRESS        PORTS   AGE
argocd-server-ingress   nginx   argocd.local    172.17.0.15    80      30s

Note

It may take a minute or two for the ADDRESS to be assigned. If you see <pending>, wait a moment and check again.

Configure access based on your operating system:

Tip

The method differs depending on your OS and driver. Choose the appropriate approach below.

For Linux or macOS/Windows with VM-based drivers (VirtualBox, Hyper-V, KVM):

# Get minikube IP
minikube ip

# Add to /etc/hosts (replace <MINIKUBE_IP> with the output from above)
echo "<MINIKUBE_IP> argocd.local" | sudo tee -a /etc/hosts

For macOS or Windows with Docker driver:

The Docker driver on macOS/Windows has network limitations that prevent direct access to the minikube IP. You need to use minikube tunnel instead.

# In a separate terminal, start the minikube tunnel
# This must stay running - don't close this terminal
minikube tunnel

Expected output:

✅  Tunnel successfully started

📌  NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...

🏃  Starting tunnel for service argocd-server-ingress.

Note

The tunnel requires sudo/administrator privileges. You may be prompted for your password.

# In a different terminal, add to /etc/hosts
echo "127.0.0.1 argocd.local" | sudo tee -a /etc/hosts

Tip

On Windows, edit C:\Windows\System32\drivers\etc\hosts as Administrator. On macOS/Linux, edit /etc/hosts with sudo.

Now open your browser and navigate to: https://argocd.local

Note

You’ll see a security warning because ArgoCD uses a self-signed certificate. Click “Advanced” → “Proceed to argocd.local” (the exact steps vary by browser).

Option B: Access via Port-Forward

If you prefer not to modify your hosts file, you can use port-forwarding instead.

# Port forward to access ArgoCD UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

Expected output:

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

Info

Keep this terminal window open. Port forwarding stops when you close it.

Open your browser and navigate to: https://localhost:8080

Note

You’ll see a security warning because ArgoCD uses a self-signed certificate. Click “Advanced” → “Proceed to localhost” (the exact steps vary by browser).

Login to ArgoCD UI

ArgoCD Dashboard

Login credentials:

  • Username: admin
  • Password: (the value from the “Get Initial Admin Password” step)

You should see the ArgoCD dashboard - currently empty because we haven’t deployed any applications yet.

ArgoCD Dashboard

(Optional) Access ArgoCD UI using CLI

# Access ArgoCD UI using CLI (if using ingress)
argocd login --insecure --username admin argocd.local

# Or if using port-forward
argocd login --insecure --username admin localhost:8080

Expected output:

Password: **********
'admin:login' logged in successfully
Context 'localhost:8080' updated

(Optional) Update ArgoCD Password

Using the CLI

# Update ArgoCD password
argocd account update-password

Expected output:

*** Enter password of currently logged in user (admin):
*** Enter new password for user admin:
*** Confirm new password for user admin:
Password updated
Context 'argocd.local' updated

Using The UI

ArgoCD - Update Password

Step 3: Deploy Your First GitOps Application

For this demo, we will be using two repositories:

This separation follows GitOps best practices: application code in one repo, Kubernetes manifests in another.

Create an ArgoCD Application

# Create ArgoCD application resource
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: deployment-demo
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/AshDevFr/deployment-demo-config.git
    targetRevision: main
    path: gitops-fundamentals/part1
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
EOF

Expected output:

application.argoproj.io/deployment-demo created

Now in the ArgoCD UI, you should see the deployment-demo application.

ArgoCD - Deployment Demo

ArgoCD - Deployment Demo

Verify Deployment

# Get the deployment
kubectl get deployment deployment-demo

Expected output:

NAME              READY   UP-TO-DATE   AVAILABLE   AGE
deployment-demo   1/1     1            1           10s

Using port forwarding, you can also access the deployment-demo application:

# Port forward to access deployment-demo application using kubectl
kubectl port-forward svc/deployment-demo 8081:8080

# Or using minikube
minikube service deployment-demo --url

Deployment Demo

Step 4: Experience GitOps Self-Healing

This is where GitOps shines. Let’s delete the deployment and watch ArgoCD automatically recreate it - demonstrating the automated reconciliation we discussed in the series introduction.

Delete the Deployment Manually

# Delete the deployment
kubectl delete deployment deployment-demo

# Check immediately - it's gone
kubectl get deployments

Expected output:

deployment.apps "deployment-demo" deleted from default namespace
No resources found in default namespace.

Watch ArgoCD Recreate It

# Wait 5-10 seconds, then check again
kubectl get deployments

Expected output:

NAME              READY   UP-TO-DATE   AVAILABLE   AGE
deployment-demo   1/1     1            1           10s

What Just Happened?

This demonstrates GitOps self-healing in action:

  1. You deleted the deployment using kubectl delete
  2. ArgoCD detected drift - the cluster state (no deployment) didn’t match Git (deployment should exist)
  3. ArgoCD automatically reconciled - it recreated the deployment to match what’s defined in Git
  4. Git remained the source of truth - your manual change was overridden

This is the power of GitOps: Git always wins. The cluster continuously converges to match what’s in Git.

Tip

Watch this in the ArgoCD UI:

  • The “Last Sync” timestamp updates when ArgoCD detects drift
  • The application’s event log shows the sync event
  • The resource tree briefly shows the deployment as missing, then recreated

Key Insight: In traditional deployments, kubectl delete would permanently remove your application. With GitOps, it’s just temporary - ArgoCD will restore it automatically because Git declares it should exist.

This is exactly the declarative configuration and automated reconciliation we explained in the series introduction. You’ve now seen it work in practice!

What’s Next

In Part 2, we’ll explore:

  • Multi-environment management (dev, staging, production)
  • Kustomize for environment-specific configurations
  • Promotion workflows between environments

Your local GitOps foundation is ready. See you in Part 2!

Resources


Series Navigation: