//------------------------------------------------------------------- //-------------------------------------------------------------------
self-hosted GitOps pipeline setup

GitOps Deployment Pipeline for Self-Hosted Kubernetes: ArgoCD Setup on Dedicated Servers

Most GitOps tutorials assume a managed cloud Kubernetes cluster with built-in load balancers, IAM-based container registry access, and ready-made secret management. In a real self-hosted GitOps pipeline setup, you get none of that for free.

This guide covers everything, including building a Kubernetes cluster on bare-metal or dedicated servers, installing ArgoCD with the latest stable manifests, wiring MetalLB and NGINX Ingress, structuring a Git repo using the app-of-apps pattern, integrating private registries and SOPS-encrypted secrets, and securing Git connectivity with Headscale/WireGuard. All tuned for a production-ready self-hosted GitOps pipeline setup.

Why Use GitOps on Bare Metal?

A self-hosted GitOps pipeline setup gives you full control over your servers, your budget, and where your data lives. Instead of relying on a cloud provider’s control plane, you run Kubernetes clusters on dedicated machines you own or rent from a provider you trust.

On bare metal, you store all infrastructure, add-ons, and application configs in Git, and ArgoCD keeps your cluster in sync with that repository. This continuous sync is the core idea behind any self-hosted GitOps pipeline setup.

If you need reliable hardware for Kubernetes and GitOps, you can run this self-hosted GitOps pipeline setup on high-performance dedicated servers built for self-managed DevOps workflows.

Prerequisites for Self-Hosted GitOps Pipeline Setup

Before you start, make sure to have the prerequisites ready for a self-hosted GitOps pipeline setup:

  • One or more dedicated servers running Ubuntu 22.04 or newer.
  • Kubernetes cluster, kubeadm or k3s, up and running, with kubectl configured.
  • Static IPs and a routable LAN segment for MetalLB.
  • Git repository, GitHub, GitLab, Gitea, etc., for your GitOps manifests.

Note: For GPU workloads inside the same self-hosted GitOps pipeline setup, you can consider dedicated GPU servers optimized for Kubernetes and AI/ML hosting.

Step 1. Create a Bare-Metal Kubernetes Cluster

If your cluster isn’t ready yet, you can follow the steps below to set up Kubernetes on bare-metal for a reliable self-hosted GitOps pipeline setup.

First, make sure the node has the right kernel networking settings and swap disabled so kubeadm’s preflight checks pass:

# Disable swap (required for kubelet)
sudo swapoff -a

# Disable swap permanently (comment out the swap line)
sudo sed -i '/ swap / s/^/#/' /etc/fstab

Then enable bridge netfilter and IP forwarding, which Kubernetes needs to route pod traffic:

# Load bridge netfilter module
sudo modprobe br_netfilter

# Configure kernel networking parameters
sudo tee /etc/sysctl.d/kubernetes.conf <<EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF

# Apply settings
sudo sysctl --system

Install Kubernetes components:

sudo mkdir -p /etc/apt/keyrings

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key \
  | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt update
sudo apt install kubeadm kubelet kubectl -y
sudo systemctl enable kubelet

Then, use the command below to initialize the Kubernetes control plane and set the IP range for pod networking in the cluster:

sudo kubeadm init --pod-network-cidr=10.244.0.0/16

Configure kubectl for your user:

mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

You must install a CNI plugin such as Flannel by applying the kube-flannel.yml manifest, which sets up pod networking across all nodes in your cluster:

kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

Finally, you must run the command below on each worker node so it can connect to the control plane and become part of the Kubernetes cluster:

kubeadm join

Once nodes are ready, you have a solid base for your self-hosted GitOps pipeline setup.

Step 2. Install MetalLB for Bare-Metal Load Balancing

MetalLB is what makes LoadBalancer services work on bare-metal, which is a critical component of a self-hosted GitOps pipeline setup.

You can install the native manifests with:

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yaml

kubectl wait --for=condition=Ready pod --all \
  -n metallb-system --timeout=180s

Then, configure the IP pool and L2 advertisement. Pick a free IP range from your LAN for this self-hosted GitOps pipeline setup:

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: baremetal-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.200-192.168.1.240
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: baremetal-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - baremetal-pool
EOF

Now, any LoadBalancer service in your self-hosted GitOps pipeline setup will receive a real IP from this pool.

For a deeper MetalLB bare-metal load balancing setup, check the full MetalLB guide for bare-metal Kubernetes.

Step 3. Install NGINX Ingress Controller with LoadBalancer Service

NGINX Ingress is the component that routes external HTTP/HTTPS traffic into services running inside your cluster. In a self-hosted GitOps pipeline setup on bare metal, you don’t get a cloud ingress or load balancer by default, so you can install NGINX Ingress yourself and expose it via a LoadBalancer service backed by MetalLB.

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer

Check the service:

kubectl get svc -n ingress-nginx

You should see an EXTERNAL-IP assigned by MetalLB, which becomes the main entrypoint for HTTP/HTTPS traffic.

Step 4. Install ArgoCD on Kubernetes

ArgoCD is the GitOps engine that keeps your cluster in sync with what’s defined in Git. In this step, you’ll install the latest stable ArgoCD release on your Kubernetes cluster so your self-hosted GitOps pipeline setup can continuously apply and reconcile manifests from your Git repository.

Once ArgoCD is running, every change you push to Git can be safely rolled out to your bare‑metal cluster with full visibility and automated rollback.

Create namespace and apply stable install manifest:

kubectl create namespace argocd

kubectl apply -n argocd --server-side --force-conflicts \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Wait for deployments/pods to be available:

kubectl wait --for=condition=available --timeout=300s \
  deployment/argocd-server -n argocd

kubectl get pods -n argocd

This gives you a current ArgoCD release suitable for a robust self-hosted GitOps pipeline setup. For HA, you can use ha/install.yaml in the same repository.

By default, argocd-server is ClusterIP. For initial configuration, use port-forwarding:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Open https://localhost:8080 in your browser. Get the admin password:

kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d && echo

Username is admin. Log in, and you’re inside the control plane of your self-hosted GitOps pipeline setup.

For more detailed information about ArgoCD installation, check the official ArgoCD Docs.

Step 5. Expose ArgoCD via Ingress

For long-term operations, you must expose ArgoCD via NGINX Ingress rather than NodePort.

Here is an example argocd-ingress.yaml file:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
  tls:
    - hosts:
        - argocd.example.com
      secretName: argocd-tls
  rules:
    - host: argocd.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 443

Apply it with:

kubectl apply -f argocd-ingress.yaml

Point argocd.example.com to the MetalLB IP of ingress-nginx. Now your self-hosted GitOps pipeline setup serves ArgoCD securely over HTTPS.

Step 6. Install the ArgoCD CLI

For fast operations in a self-hosted GitOps pipeline setup, you can install the ArgoCD CLI from the latest release:

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

argocd version --short --client

Use argocd login with your ingress or port-forward endpoint to manage applications:

argocd login argocd.example.com --username admin --password YOUR_PASSWORD --grpc-web

Structure Git for a Multi-App ArgoCD Setup

The app-of-apps pattern is a way to structure your Git repository so one ArgoCD Application can manage many other Applications. This makes your self-hosted GitOps pipeline setup easier to set up, easier to grow, and much simpler to keep consistent across environments.

Suggested layout includes:

gitops/
  clusters/
    prod/
      root-app.yaml
      apps/
        metallb-app.yaml
        ingress-app.yaml
        infra-storage-app.yaml
        workloads-app.yaml
  apps/
    metallb/
      values.yaml
      kustomization.yaml
    ingress-nginx/
      values.yaml
      kustomization.yaml
    my-app/
      deployment.yaml
      service.yaml
      ingress.yaml
  infra/
    storageclass/
      local-path-sc.yaml
    secrets/
      sops/
        registry-credentials.yaml

This structure makes Git the single place where you define everything in your self-hosted GitOps pipeline setup, such as your core infrastructure, extra add-ons, and all application workloads.

Root app-of-apps Application:

This is the entry point for your GitOps setup. You define it in clusters/prod/root-app.yaml inside your Git repo, and it tells ArgoCD to manage everything under clusters/prod/ as a group of child applications. When you sync this root app, ArgoCD automatically creates and updates all the other apps and infrastructure pieces in your self-hosted GitOps pipeline setup.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops.git
    targetRevision: main
    path: clusters/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Private Container Registries Without Cloud IAM

In a self-hosted GitOps pipeline setup, image pulls often hit a self-hosted registry without IAM integration. You must declare registry credentials explicitly.

Create the secret:

kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=your_user \
  --docker-password=your_password \
  --docker-email=[email protected] \
  -n your-app-namespace

Reference it in your workload manifest stored in Git:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: your-app-namespace
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: my-app
          image: registry.example.com/your-org/my-app:latest
          ports:
            - containerPort: 8080

ArgoCD syncs this, and your pods can pull private images.

Secrets Management with SOPS

Because your self-hosted GitOps pipeline setup doesn’t necessarily include a managed secret store, SOPS lets you safely keep secrets in Git.

  1. Install SOPS locally and pick a key backend, age, or GPG.
  2. Write Kubernetes Secret manifests with plaintext values.
  3. Run sops -e secret.yaml > secret.enc.yaml and commit the encrypted file.
  4. Deploy a SOPS-aware controller, such as ksops, sops-operator, or a Helm plugin, in your cluster.

When ArgoCD syncs, it applies the encrypted secret manifests, the SOPS controller decrypts them, and the workloads get their secrets, all while your self-hosted GitOps pipeline setup stays fully defined in Git and easy to review.

Storage Classes and Persistent Volumes for Bare Metal

On bare-metal Kubernetes, you don’t get cloud storage classes or managed disks. To run databases and other stateful apps in a self-hosted GitOps pipeline setup, you must define your own StorageClasses and PersistentVolumes so the cluster knows where and how to create data volumes.

For single-node or simple lab setups:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer

Save this YAML as infra/storageclass/local-path-sc.yaml in your Git repo, and let ArgoCD deploy it through an infra-storage-app Application. Then any PVC that uses storageClassName: local-path will get local disk, so your stateful apps are fully supported in your self-hosted GitOps pipeline setup.

For clusters with multiple nodes, use shared storage like NFS or Ceph/Rook, and manage its configuration in Git as part of your self-hosted GitOps pipeline setup.

Secure Git Connectivity

If your cluster is on a private network, your self-hosted GitOps pipeline setup still needs a secure way to reach the Git server. Headscale, a self-hosted Tailscale control server, lets you do that using access rules and an encrypted WireGuard network.

Typical approach:

  • Deploy Headscale and join Kubernetes nodes and the Git server/VM as clients.
  • Use Headscale ACLs to restrict which IPs can reach Git.
  • Write Kubernetes NetworkPolicy objects to allow egress from argocd namespace to Headscale IP ranges and block other outbound access.

If you want a full setup of deploying Headscale as your self-hosted mesh VPN control plane, check this guide on self-hosting Headscale on Ubuntu, including TLS, reverse proxy, and client setup.

Connect ArgoCD to the GitOps Repository

Now you must wire ArgoCD to the Git repository that defines your self-hosted GitOps pipeline setup.

Register HTTPS repository:

argocd repo add https://github.com/your-org/gitops.git

Register SSH repository:

argocd repo add [email protected]:your-org/gitops.git \
  --ssh-private-key-path ~/.ssh/id_rsa

Create root app from CLI:

argocd app create root-app \
  --repo https://github.com/your-org/gitops.git \
  --path clusters/prod \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace argocd \
  --sync-policy automated

Sync it:

argocd app sync root-app

ArgoCD Automated Rollback and Health Checks

Use ArgoCD’s automatic sync and self-heal features with health checks so your self-hosted GitOps pipeline setup can recover quickly from bad deployments.

In Application specs for critical workloads:

spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

ArgoCD keeps checking whether your resources are healthy, and if a new change breaks things, it automatically goes back to the last working version. That way, your self-hosted GitOps pipeline setup can stay reliable.

Conclusion

Building a modern self-hosted GitOps pipeline setup on bare-metal or dedicated servers means solving things that cloud Kubernetes usually does for you, such as load balancing without a cloud LB, storage classes without cloud disks, private registry access without IAM, and secure Git access without a managed service mesh.

By using ArgoCD, MetalLB, NGINX Ingress, SOPS, and Headscale/WireGuard together, you get a Git‑driven, health‑checked, fully self-managed GitOps pipeline setup that’s ready for production.

We hope you enjoy this guide.

FAQs

How do I keep secrets safe in Git?

Use SOPS to encrypt your secret YAML files, and run a controller in the cluster that can decrypt them when ArgoCD applies the manifests.

Can ArgoCD manage multiple clusters?

Yes. You can add multiple cluster contexts and manage them via projects and Applications.

Is this self-hosted GitOps pipeline setup compatible with k3s?

Yes. All components work on k3s as well, with the same GitOps patterns.

Post Your Comment

PerLod delivers high-performance hosting with real-time support and unmatched reliability.

Contact us

Payment methods

payment gateway
Perlod Logo
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.