<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>I&apos;m Gatan</title><description>Personal Site &amp; Blog</description><link>https://im-gatan.com/</link><language>en</language><item><title>Setting Up ArgoCD with NGINX Ingress on a Bare-Metal Kubernetes Cluster</title><link>https://im-gatan.com/posts/argocd-setup-guide/</link><guid isPermaLink="true">https://im-gatan.com/posts/argocd-setup-guide/</guid><description>A complete guide to installing ArgoCD on a bare-metal Kubernetes cluster, configuring NGINX Ingress Controller for web access, and deploying your first applications using both the CLI and Web UI.</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This guide walks you through setting up &lt;strong&gt;ArgoCD&lt;/strong&gt; — a declarative, GitOps-based continuous delivery tool — on a &lt;strong&gt;bare-metal Kubernetes cluster&lt;/strong&gt;. By the end, you&apos;ll have ArgoCD fully accessible through a web browser via an NGINX Ingress Controller, and you&apos;ll deploy sample applications using both the &lt;strong&gt;CLI&lt;/strong&gt; and the &lt;strong&gt;Web UI&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This guide assumes you already have a running Kubernetes cluster (e.g., set up with kubeadm). If you don&apos;t have one yet, check out my previous post on &lt;a href=&quot;/posts/kubernetes-single-host-setup/&quot;&gt;Kubernetes Single-Node Setup&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-installing-argocd&quot;&gt;Installing ArgoCD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-setting-up-nginx-ingress-controller&quot;&gt;Setting Up NGINX Ingress Controller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-creating-an-ingress-for-argocd&quot;&gt;Creating an Ingress for ArgoCD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-installing-the-argocd-cli&quot;&gt;Installing the ArgoCD CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-initial-login-and-password-setup&quot;&gt;Initial Login and Password Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-accessing-the-argocd-web-ui&quot;&gt;Accessing the ArgoCD Web UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-tuning-the-refresh-interval&quot;&gt;Tuning the Refresh Interval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-deploying-an-application-via-cli&quot;&gt;Deploying an Application via CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#9-deploying-an-application-via-web-ui&quot;&gt;Deploying an Application via Web UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#10-argocd-cli-tips&quot;&gt;ArgoCD CLI Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Installing ArgoCD&lt;/h2&gt;
&lt;h3&gt;1.1 Download the Installation Manifest&lt;/h3&gt;
&lt;p&gt;First, download the official ArgoCD installation manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 Configure a Custom Root Path (Optional)&lt;/h3&gt;
&lt;p&gt;If you want to access ArgoCD through a &lt;strong&gt;sub-path&lt;/strong&gt; (e.g., &lt;code&gt;http://&amp;lt;your-ip&amp;gt;/argocd-lab&lt;/code&gt;) instead of using a dedicated domain, you&apos;ll need to modify the &lt;code&gt;argocd-server&lt;/code&gt; Deployment in the downloaded &lt;code&gt;install.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Find the &lt;code&gt;Deployment&lt;/code&gt; resource named &lt;code&gt;argocd-server&lt;/code&gt; and add the &lt;code&gt;--rootpath&lt;/code&gt;, &lt;code&gt;--basehref&lt;/code&gt;, and &lt;code&gt;--insecure&lt;/code&gt; arguments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: server
    app.kubernetes.io/name: argocd-server
    app.kubernetes.io/part-of: argocd
  name: argocd-server
...
# Add these under spec.template.spec.containers[0].args
spec:
     containers:
     - args:
       - /usr/local/bin/argocd-server
       - --staticassets
       - /shared/app
       - --insecure
       - --rootpath
       - /argocd-lab
       - --basehref
       - /argocd-lab
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The path &lt;code&gt;/argocd-lab&lt;/code&gt; is used in this guide as an example to avoid conflicts with ArgoCD&apos;s default &lt;code&gt;/argocd&lt;/code&gt; path. Feel free to change it to whatever path suits your needs — just make sure it stays consistent across your Ingress configuration and CLI login commands. If you&apos;re using a dedicated domain or want ArgoCD on the root URL, you can skip the &lt;code&gt;--rootpath&lt;/code&gt; and &lt;code&gt;--basehref&lt;/code&gt; flags entirely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.3 Create the Namespace and Deploy&lt;/h3&gt;
&lt;p&gt;Create the &lt;code&gt;argocd&lt;/code&gt; namespace and apply the manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create namespace argocd
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f install.yaml -n argocd
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Setting Up NGINX Ingress Controller&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A Note on NGINX Ingress:&lt;/strong&gt; We are using NGINX Ingress Controller here because it is very easy to set up for demonstration purposes. However, if you are planning to deploy this in a production environment, you should consider alternatives that provide better long-term security support and active maintenance (such as Traefik, HAProxy, Contour, etc.).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important for bare-metal/on-prem clusters:&lt;/strong&gt; Clusters bootstrapped with &lt;strong&gt;kubeadm&lt;/strong&gt; don&apos;t come with a built-in Ingress Controller. Without one, any &lt;code&gt;Ingress&lt;/code&gt; resource you create won&apos;t work — &lt;code&gt;kubectl get ingress&lt;/code&gt; will show no &lt;code&gt;ADDRESS&lt;/code&gt;, and you&apos;ll get &lt;code&gt;connection refused&lt;/code&gt; errors when trying to access services.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.1 Check If an Ingress Controller Exists&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl get ingressclass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the output is &lt;code&gt;No resources found&lt;/code&gt;, you need to install one.&lt;/p&gt;
&lt;h3&gt;2.2 Install NGINX Ingress Controller&lt;/h3&gt;
&lt;p&gt;Since we&apos;re on bare-metal (no cloud LoadBalancer), install the NGINX Ingress Controller using the bare-metal manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.3/deploy/static/provider/baremetal/deploy.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 Enable Host Networking&lt;/h3&gt;
&lt;p&gt;Edit the &lt;code&gt;ingress-nginx-controller&lt;/code&gt; Deployment to enable &lt;code&gt;hostNetwork&lt;/code&gt;, which allows the controller to bind directly to port 80/443 on the node — no external LoadBalancer or MetalLB required:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl edit deployment ingress-nginx-controller -n ingress-nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;hostNetwork: true&lt;/code&gt; under &lt;code&gt;spec.template.spec&lt;/code&gt;, at the same level as &lt;code&gt;containers&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spec:
  template:
    spec:
      hostNetwork: true
      containers:
      - name: controller
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 Verify the Installation&lt;/h3&gt;
&lt;p&gt;Wait a moment, then confirm the controller pod is running and the IngressClass is registered:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -n ingress-nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kubectl get ingressclass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME    CONTROLLER             PARAMETERS   AGE
nginx   k8s.io/ingress-nginx   &amp;lt;none&amp;gt;       30s
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Creating an Ingress for ArgoCD&lt;/h2&gt;
&lt;p&gt;Create an &lt;code&gt;ingress.yaml&lt;/code&gt; file to expose ArgoCD through the &lt;code&gt;/argocd-lab&lt;/code&gt; path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
  annotations:
    ingress.kubernetes.io/ssl-redirect: &quot;false&quot;
spec:
  rules:
  - http:
      paths:
      - path: /argocd-lab
        pathType: Prefix
        backend:
          service:
            name: argocd-server
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f ingress.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Installing the ArgoCD CLI&lt;/h2&gt;
&lt;p&gt;Download and install the ArgoCD CLI binary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -SL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clean up the downloaded file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm argocd-linux-amd64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the installation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd: v3.4.4+443415b
  BuildDate: 2026-06-18T09:15:00Z
  GitCommit: 443415b5527ac55366e0760c93ef0e1abd0cf273
  GitTreeState: clean
  GoVersion: go1.26.0
  Compiler: gc
  Platform: linux/amd64
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You may see a &lt;code&gt;fatal&lt;/code&gt; message about the server address being unspecified — this is normal before logging in.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Initial Login and Password Setup&lt;/h2&gt;
&lt;h3&gt;5.1 Retrieve the Default Admin Password&lt;/h3&gt;
&lt;p&gt;ArgoCD generates a random initial password for the &lt;code&gt;admin&lt;/code&gt; account during installation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd admin initial-password -n argocd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PASSWORD]

This password must be only used for first time login. We strongly recommend
you update the password using `argocd account update-password`.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 Log In via CLI&lt;/h3&gt;
&lt;p&gt;Log in using the generated password. Replace &lt;code&gt;&amp;lt;your-ip&amp;gt;&lt;/code&gt; with your node&apos;s IP address:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd login &amp;lt;your-ip&amp;gt;:80 --grpc-web-root-path=/argocd-lab --plaintext --insecure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When prompted, enter &lt;code&gt;admin&lt;/code&gt; as the username and the password from the previous step.&lt;/p&gt;
&lt;h3&gt;5.3 Change the Admin Password&lt;/h3&gt;
&lt;p&gt;For security, update the default password immediately:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd account update-password
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;*** Enter password of currently logged in user (admin):
*** Enter new password for user admin:
*** Confirm new password for user admin:
Password updated
Context &apos;&amp;lt;your-ip&amp;gt;:80/argocd-lab&apos; updated
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Accessing the ArgoCD Web UI&lt;/h2&gt;
&lt;p&gt;Open your browser and navigate to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://&amp;lt;your-ip&amp;gt;/argocd-lab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Log in with the &lt;code&gt;admin&lt;/code&gt; username and the password you just set.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./argocd-login.png&quot; alt=&quot;ArgoCD Login Page&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Tuning the Refresh Interval&lt;/h2&gt;
&lt;p&gt;By default, ArgoCD polls your Git repositories every &lt;strong&gt;3 minutes&lt;/strong&gt; to check for changes. If you want faster feedback (e.g., during development), you can lower this interval.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;config.yaml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  timeout.reconciliation: 20s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f config.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl describe configmap -n argocd argocd-cm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the repo server to pick up the new setting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl -n argocd rollout restart deploy argocd-repo-server
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; A 20-second interval is great for development and testing, but for production environments you may want to keep it at the default 3 minutes (or higher) to reduce load on both ArgoCD and your Git server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Deploying an Application via CLI&lt;/h2&gt;
&lt;h3&gt;8.1 Create the Application&lt;/h3&gt;
&lt;p&gt;Create a sample application called &lt;code&gt;simple-deploy&lt;/code&gt; pointing to a Git repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd app create simple-deploy \
  --repo https://github.com/maliqpotter/argo-simple-project.git \
  --path simple-demo \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace default
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;application &apos;simple-deploy&apos; created
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2 Check the Application Status&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;argocd app get simple-deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The initial status will be &lt;code&gt;OutOfSync&lt;/code&gt; because no Kubernetes resources have been created yet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Name:               argocd/simple-deploy
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
Source:
- Repo:             https://github.com/maliqpotter/argo-simple-project.git
  Path:             simple-demo
Sync Status:        OutOfSync from  (1873ef1)
Health Status:      Missing

GROUP  KIND        NAMESPACE  NAME          STATUS     HEALTH   HOOK  MESSAGE
apps   Deployment  default    simple-nginx  OutOfSync  Missing
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.3 Sync (Deploy) the Application&lt;/h3&gt;
&lt;p&gt;Trigger a sync to deploy the application:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd app sync simple-deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the sync completes, verify the status:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd app get simple-deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Name:               argocd/simple-deploy
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
Source:
- Repo:             https://github.com/maliqpotter/argo-simple-project.git
  Path:             simple-demo
Sync Policy:        Manual
Sync Status:        Synced to  (1873ef1)
Health Status:      Healthy

GROUP  KIND        NAMESPACE  NAME          STATUS  HEALTH   HOOK  MESSAGE
apps   Deployment  default    simple-nginx  Synced  Healthy        deployment.apps/simple-nginx created
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.4 Verify the Resources&lt;/h3&gt;
&lt;p&gt;Check that the pods and deployment were created:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pod -n default
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NAME                             READY   STATUS    RESTARTS   AGE
simple-deploy-5d7468b6f8-75w68   1/1     Running   0          2m11s
simple-deploy-5d7468b6f8-mkkph   1/1     Running   0          2m11s
simple-deploy-5d7468b6f8-mkgdz   1/1     Running   0          2m11s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kubectl get deployment -n default
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NAME            READY   UP-TO-DATE   AVAILABLE   AGE
simple-deploy   3/3     3            3           2m38s
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;9. Deploying an Application via Web UI&lt;/h2&gt;
&lt;p&gt;In addition to the CLI, you can create and deploy applications directly from the ArgoCD dashboard.&lt;/p&gt;
&lt;h3&gt;9.1 Open the Dashboard&lt;/h3&gt;
&lt;p&gt;After logging in, you&apos;ll see the ArgoCD dashboard showing your existing applications (like the &lt;code&gt;simple-deploy&lt;/code&gt; app we just created via CLI).&lt;/p&gt;
&lt;p&gt;Click the &lt;strong&gt;&lt;code&gt;+ NEW APP&lt;/code&gt;&lt;/strong&gt; button to create a new application.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./argocd-dashboard.png&quot; alt=&quot;ArgoCD Dashboard with NEW APP button highlighted&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;9.2 Fill in the General Settings&lt;/h3&gt;
&lt;p&gt;Under the &lt;strong&gt;GENERAL&lt;/strong&gt; section, configure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application Name&lt;/strong&gt;: &lt;code&gt;nginx-with-ingress&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project Name&lt;/strong&gt;: &lt;code&gt;default&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync Policy&lt;/strong&gt;: &lt;code&gt;Manual&lt;/code&gt; (or &lt;code&gt;Automatic&lt;/code&gt; if you want auto-sync)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./argocd-new-app-general.png&quot; alt=&quot;ArgoCD New App - General Configuration&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;9.3 Configure the Source and Destination&lt;/h3&gt;
&lt;p&gt;Under the &lt;strong&gt;SOURCE&lt;/strong&gt; section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repository URL&lt;/strong&gt;: &lt;code&gt;https://github.com/maliqpotter/argo-simple-project.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revision&lt;/strong&gt;: &lt;code&gt;HEAD&lt;/code&gt; (or the specific branch you want, e.g., &lt;code&gt;main&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path&lt;/strong&gt;: &lt;code&gt;nginx-with-ingress&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under the &lt;strong&gt;DESTINATION&lt;/strong&gt; section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cluster URL&lt;/strong&gt;: &lt;code&gt;https://kubernetes.default.svc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Namespace&lt;/strong&gt;: &lt;code&gt;default&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then click the &lt;strong&gt;&lt;code&gt;CREATE&lt;/code&gt;&lt;/strong&gt; button at the top of the form.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./argocd-new-app-source.png&quot; alt=&quot;ArgoCD New App - Source and Destination Configuration&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;9.4 Sync the Application&lt;/h3&gt;
&lt;p&gt;Once the &lt;code&gt;nginx-with-ingress&lt;/code&gt; application appears on the dashboard with an &lt;code&gt;OutOfSync&lt;/code&gt; status, click the &lt;strong&gt;&lt;code&gt;SYNC&lt;/code&gt;&lt;/strong&gt; button on the application card, then click &lt;strong&gt;&lt;code&gt;SYNCHRONIZE&lt;/code&gt;&lt;/strong&gt; to confirm.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./argocd-sync-app.png&quot; alt=&quot;ArgoCD Dashboard showing the Sync button&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After the sync completes, the application status will change to &lt;code&gt;Synced&lt;/code&gt; and &lt;code&gt;Healthy&lt;/code&gt;. You can click into the application to see a visual map of all the Kubernetes resources that were created (Deployments, ReplicaSets, Pods, etc.).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;10. ArgoCD CLI Tips&lt;/h2&gt;
&lt;h3&gt;10.1 Set Default Namespace&lt;/h3&gt;
&lt;p&gt;Since ArgoCD is installed in the &lt;code&gt;argocd&lt;/code&gt; namespace, you can set it as your default to avoid typing &lt;code&gt;-n argocd&lt;/code&gt; on every command:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option A — Set the kubectl context:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl config set-context --current --namespace=argocd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Option B — Use an environment variable:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export ARGOCD_OPTS=&apos;--port-forward-namespace argocd&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10.2 Enable Bash Completion&lt;/h3&gt;
&lt;p&gt;Speed up your CLI workflow with tab completion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;argocd completion bash | sudo tee /etc/bash_completion.d/argocd
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/&quot;&gt;ArgoCD Official Documentation — argoproj.github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.github.io/ingress-nginx/&quot;&gt;NGINX Ingress Controller — kubernetes.github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd/&quot;&gt;ArgoCD CLI Reference — argo-cd.readthedocs.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This guide is based on a real-world deployment. Adjust IPs, paths, and version numbers to match your own environment.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>Kubernetes Single-Node Setup on AlmaLinux 9.8 with Kubeadm, Containerd &amp; Calico</title><link>https://im-gatan.com/posts/kubernetes-single-host-setup/</link><guid isPermaLink="true">https://im-gatan.com/posts/kubernetes-single-host-setup/</guid><description>A step-by-step guide to deploying a single-node Kubernetes cluster using Kubeadm, containerd as the container runtime, and Tigera Calico as the CNI plugin on AlmaLinux 9.8.</description><pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This guide walks you through setting up a fully functional &lt;strong&gt;single-node Kubernetes cluster&lt;/strong&gt; using &lt;strong&gt;Kubeadm&lt;/strong&gt;, with &lt;strong&gt;containerd&lt;/strong&gt; as the container runtime and &lt;strong&gt;Tigera Calico&lt;/strong&gt; as the CNI plugin — all running on &lt;strong&gt;AlmaLinux 9.8 (Olive Jaguar)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;By the end, you&apos;ll have a working Kubernetes node capable of scheduling and running pods, complete with networking provided by Calico.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Server Specifications&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AlmaLinux 9.8 (Olive Jaguar)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container Runtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;containerd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CNI Plugin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tigera Calico&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Installer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kubeadm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-system-preparation&quot;&gt;System Preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-installing-containerd&quot;&gt;Installing containerd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-configuring-containerd-for-kubernetes&quot;&gt;Configuring containerd for Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-installing-kubeadm-kubelet-kubectl&quot;&gt;Installing kubeadm, kubelet, kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-initializing-the-cluster-with-kubeadm&quot;&gt;Initializing the Cluster with kubeadm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-configuring-kubectl&quot;&gt;Configuring kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-installing-tigera-calico&quot;&gt;Installing Tigera Calico&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-verifying-the-cluster&quot;&gt;Verifying the Cluster&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;1. System Preparation&lt;/h2&gt;
&lt;h3&gt;1.1 Set Hostname&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;hostnamectl set-hostname &amp;lt;your-hostname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 Disable Swap&lt;/h3&gt;
&lt;p&gt;Kubernetes requires swap to be disabled.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Disable swap immediately
swapoff -a

# Disable swap permanently
sed -i &apos;/ swap / s/^\(.*\)$/#\1/g&apos; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 Disable SELinux&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Disable temporarily
setenforce 0

# Disable permanently
sed -i &apos;s/^SELINUX=enforcing$/SELINUX=permissive/&apos; /etc/selinux/config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4 Disable Firewall (or Configure Required Ports)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl disable --now firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you prefer to keep the firewall active, open the following ports instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;6443/tcp&lt;/code&gt; — Kubernetes API Server&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2379-2380/tcp&lt;/code&gt; — etcd server client API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10250/tcp&lt;/code&gt; — kubelet API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10251/tcp&lt;/code&gt; — kube-scheduler&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10252/tcp&lt;/code&gt; — kube-controller-manager&lt;/li&gt;
&lt;li&gt;&lt;code&gt;179/tcp&lt;/code&gt; — Calico BGP&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4789/udp&lt;/code&gt; — Calico VXLAN (optional)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.5 Load Required Kernel Modules&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Create module configuration
cat &amp;lt;&amp;lt;EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# Load modules immediately
modprobe overlay
modprobe br_netfilter
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.6 Configure Sysctl Parameters&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# Apply the configuration
sysctl --system
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.7 Verify Kernel Modules Are Loaded&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;lsmod | grep br_netfilter
lsmod | grep overlay
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output (values may vary):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;br_netfilter           36864  0
bridge                421888  1 br_netfilter
overlay               237568  0
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Installing containerd&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href=&quot;https://docs.docker.com/engine/install/rhel/&quot;&gt;Docker Engine on RHEL — docs.docker.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since we only need &lt;strong&gt;containerd&lt;/strong&gt; (not the full Docker Engine), we&apos;ll use the Docker repository to install just the &lt;code&gt;containerd.io&lt;/code&gt; package.&lt;/p&gt;
&lt;h3&gt;2.1 Remove Potentially Conflicting Packages&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dnf remove -y docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine \
    podman \
    runc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 Install DNF Plugin and Add Docker Repository&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dnf -y install dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 Install containerd.io&lt;/h3&gt;
&lt;p&gt;Only install the &lt;code&gt;containerd.io&lt;/code&gt; package — no need for docker-ce or other Docker components.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dnf install -y containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 Enable and Start containerd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable --now containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 Verify containerd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl status containerd
containerd --version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Configuring containerd for Kubernetes&lt;/h2&gt;
&lt;h3&gt;3.1 Generate Default Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 Enable SystemdCgroup&lt;/h3&gt;
&lt;p&gt;Kubernetes requires containerd to use &lt;code&gt;systemd&lt;/code&gt; as the cgroup driver.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sed -i &apos;s/SystemdCgroup = false/SystemdCgroup = true/&apos; /etc/containerd/config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep &apos;SystemdCgroup&apos; /etc/containerd/config.toml
# Expected output: SystemdCgroup = true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 Restart containerd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart containerd
systemctl status containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Installing kubeadm, kubelet, kubectl&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;Installing kubeadm — kubernetes.io&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.1 Add the Kubernetes Repository&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Replace &lt;code&gt;v1.36&lt;/code&gt; with your desired Kubernetes version. Check the latest releases at &lt;a href=&quot;https://kubernetes.io/releases/&quot;&gt;kubernetes.io/releases&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.2 Install kubelet, kubeadm, kubectl&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 Enable kubelet&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable --now kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; kubelet will enter a restart loop until &lt;code&gt;kubeadm init&lt;/code&gt; completes — this is expected behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.4 Verify Versions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubeadm version
kubectl version --client
kubelet --version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Initializing the Cluster with kubeadm&lt;/h2&gt;
&lt;h3&gt;5.1 Initialize the Control Plane&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --cri-socket unix:///run/containerd/containerd.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;--pod-network-cidr=10.244.0.0/16&lt;/code&gt; flag is used here because some server networks (e.g., &lt;code&gt;192.168.x.x/24&lt;/code&gt;) may overlap with Calico&apos;s default CIDR (&lt;code&gt;192.168.0.0/16&lt;/code&gt;). Using &lt;code&gt;10.244.0.0/16&lt;/code&gt; avoids routing conflicts.&lt;/li&gt;
&lt;li&gt;If your server has multiple network interfaces, add &lt;code&gt;--apiserver-advertise-address=&amp;lt;your-server-ip&amp;gt;&lt;/code&gt; to specify which IP the API server should bind to.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Make sure to save the &lt;code&gt;kubeadm join&lt;/code&gt; command from the output — you&apos;ll need it if you ever want to add worker nodes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Configuring kubectl&lt;/h2&gt;
&lt;h3&gt;6.1 Set Up kubeconfig for Root User&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 Verify the Node (NotReady Is Expected)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME              STATUS     ROLES           AGE   VERSION
&amp;lt;your-hostname&amp;gt;   NotReady   control-plane   Xs    v1.36.x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;NotReady&lt;/code&gt; status is expected because the CNI plugin (Calico) hasn&apos;t been installed yet.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Installing Tigera Calico&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; &lt;a href=&quot;https://docs.tigera.io/calico/latest/getting-started/kubernetes/k8s-single-node&quot;&gt;Calico on single-host Kubernetes — docs.tigera.io&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7.1 Install the Tigera Operator&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/tigera-operator.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2 Create the Custom Resource for Calico&lt;/h3&gt;
&lt;p&gt;Since we&apos;re using CIDR &lt;code&gt;10.244.0.0/16&lt;/code&gt; (instead of the default &lt;code&gt;192.168.0.0/16&lt;/code&gt;), download the manifest first, update the CIDR, then apply:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/custom-resources.yaml

# Replace Calico&apos;s default CIDR with ours
sed -i &apos;s|192.168.0.0/16|10.244.0.0/16|g&apos; custom-resources.yaml

# Verify the change
grep &apos;cidr&apos; custom-resources.yaml

# Apply the configuration
kubectl create -f custom-resources.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.3 Wait for Calico to Deploy&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;watch kubectl get pods -n calico-system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait until all pods show &lt;code&gt;Running&lt;/code&gt; status:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-xxx                1/1     Running   0          Xm
calico-node-xxx                            1/1     Running   0          Xm
calico-typha-xxx                           1/1     Running   0          Xm
csi-node-driver-xxx                        2/2     Running   0          Xm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; to exit watch.&lt;/p&gt;
&lt;h3&gt;7.4 Untaint the Control Plane Node&lt;/h3&gt;
&lt;p&gt;By default, Kubernetes prevents workloads from being scheduled on control plane nodes. For a single-node setup, we need to remove this taint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl taint nodes --all node-role.kubernetes.io/control-plane-
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Verifying the Cluster&lt;/h2&gt;
&lt;h3&gt;8.1 Check Node Status&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl get nodes -o wide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The node status should now show &lt;code&gt;Ready&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME              STATUS   ROLES           AGE   VERSION   INTERNAL-IP     OS-IMAGE                      CONTAINER-RUNTIME
&amp;lt;your-hostname&amp;gt;   Ready    control-plane   Xm    v1.36.x   &amp;lt;your-ip&amp;gt;      AlmaLinux 9.8 (Olive Jaguar)  containerd://2.x.x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2 Check All System Pods&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All pods across &lt;code&gt;kube-system&lt;/code&gt;, &lt;code&gt;calico-system&lt;/code&gt;, and &lt;code&gt;tigera-operator&lt;/code&gt; namespaces should show &lt;code&gt;Running&lt;/code&gt; status.&lt;/p&gt;
&lt;h3&gt;8.3 Test with a Simple Deployment&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl run nginx --image=nginx --port=80
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          15s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the pod details:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl describe pod nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pod should be scheduled on your node with a Calico-assigned IP from the &lt;code&gt;10.244.x.x&lt;/code&gt; range.&lt;/p&gt;
&lt;h3&gt;8.4 Check Cluster Information&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl cluster-info
kubectl version
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;Node remains NotReady after installing Calico&lt;/h3&gt;
&lt;p&gt;Inspect the node events and Calico logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl describe node &amp;lt;your-hostname&amp;gt;
kubectl logs -n calico-system -l k8s-app=calico-node
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pod stuck in ContainerCreating&lt;/h3&gt;
&lt;p&gt;Check the pod events and kubelet logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt;
journalctl -u kubelet -f
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reset the cluster (start over)&lt;/h3&gt;
&lt;p&gt;If you need to completely reset and start fresh:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubeadm reset -f
rm -rf /etc/cni/net.d
rm -rf $HOME/.kube
iptables -F &amp;amp;&amp;amp; iptables -t nat -F &amp;amp;&amp;amp; iptables -t mangle -F &amp;amp;&amp;amp; iptables -X
ipvsadm --clear 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/rhel/&quot;&gt;Docker Engine on RHEL — docs.docker.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;Installing kubeadm — kubernetes.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tigera.io/calico/latest/getting-started/kubernetes/k8s-single-node&quot;&gt;Calico on single-host Kubernetes — docs.tigera.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This guide is based on a real-world deployment. Adjust hostnames, IPs, and version numbers to match your own environment.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>Deploying OpenStack with Ceph Tentacle: A 3-Node Converged Setup Guide</title><link>https://im-gatan.com/posts/openstack-ceph-deployment/</link><guid isPermaLink="true">https://im-gatan.com/posts/openstack-ceph-deployment/</guid><description>A complete step-by-step guide to deploying OpenStack (master/2025.2) with Ceph Tentacle as the distributed storage backend on a 3-node converged cluster using Kolla-Ansible and Ubuntu Noble 24.04.</description><pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this guide, I&apos;ll walk you through deploying a fully functional &lt;strong&gt;OpenStack&lt;/strong&gt; cloud platform integrated with &lt;strong&gt;Ceph Tentacle&lt;/strong&gt; as the distributed storage backend — all on a 3-node converged cluster running &lt;strong&gt;Ubuntu Noble 24.04&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; align-items: center; gap: 2rem; margin: 1.5rem 0;&quot;&amp;gt;
&amp;lt;img src=&quot;/images/blog/ceph-logo.png&quot; alt=&quot;Ceph Logo&quot; style=&quot;height: 120px; width: auto;&quot; /&amp;gt;
&amp;lt;span style=&quot;font-size: 2rem; opacity: 0.4;&quot;&amp;gt;+&amp;lt;/span&amp;gt;
&amp;lt;img src=&quot;/images/blog/openstack-logo.png&quot; alt=&quot;OpenStack Logo&quot; style=&quot;height: 120px; width: auto;&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;This setup uses &lt;strong&gt;Kolla-Ansible&lt;/strong&gt; for deploying OpenStack services as Docker containers, and &lt;strong&gt;cephadm&lt;/strong&gt; for bootstrapping the Ceph cluster. By the end of this guide, you&apos;ll have a working private cloud with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;☁️ &lt;strong&gt;OpenStack&lt;/strong&gt; — Keystone, Glance, Nova, Neutron, Cinder, Horizon&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Ceph&lt;/strong&gt; — Distributed storage for images, volumes, VMs, and backups&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Networking&lt;/strong&gt; — Provider + tenant networks with floating IPs&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;VM Instances&lt;/strong&gt; — Running CirrOS test instances with SSH access&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-topology--specifications&quot;&gt;Topology &amp;amp; Specifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-node-preparation&quot;&gt;Node Preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-deploy-ceph-tentacle-with-cephadm&quot;&gt;Deploy Ceph Tentacle with cephadm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-ceph-pool--keyring-setup-for-openstack&quot;&gt;Ceph Pool &amp;amp; Keyring Setup for OpenStack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-install-kolla-ansible&quot;&gt;Install Kolla-Ansible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-kolla-ansible-configuration&quot;&gt;Kolla-Ansible Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-external-ceph-integration-with-kolla-ansible&quot;&gt;External Ceph Integration with Kolla-Ansible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-deploy-openstack&quot;&gt;Deploy OpenStack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#9-openstack-network-setup&quot;&gt;OpenStack Network Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#10-create-vm-instance&quot;&gt;Create VM Instance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#11-verify-vm-connectivity&quot;&gt;Verify VM Connectivity&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Topology &amp;amp; Specifications&lt;/h2&gt;
&lt;h3&gt;Network Topology&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;                    ┌──────────────────┐
                    │   Proxmox Host   │
                    │  vmbr0 (eno1)    │
                    │ 192.168.205.253  │
                    └────────┬─────────┘
           ┌─────────────────┼─────────────────┐
           │                 │                 │
    ens18 (Mgmt)       ens18 (Mgmt)       ens18 (Mgmt)
  192.168.205.101    192.168.205.102    192.168.205.103
    ens19 (Provider)   ens19 (Provider)   ens19 (Provider)
    (no IP)            (no IP)            (no IP)
           │                 │                 │
  ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
  │  ops-ceph-ctrl  │ │ops-ceph-comp1│ │ops-ceph-comp2│
  │  (controller)   │ │  (compute)   │ │  (compute)   │
  │                 │ │              │ │              │
  │ /dev/sda (OS)   │ │ /dev/sda(OS) │ │ /dev/sda(OS) │
  │ /dev/sdb OSD.0  │ │ /dev/sdb OSD │ │ /dev/sdb OSD │
  │ /dev/sdc OSD.1  │ │ /dev/sdc OSD │ │ /dev/sdc OSD │
  │ /dev/sdd OSD.2  │ │ /dev/sdd OSD │ │ /dev/sdd OSD │
  └─────────────────┘ └──────────────┘ └──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node Specifications&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;ops-ceph-ctrl&lt;/th&gt;
&lt;th&gt;ops-ceph-comp1&lt;/th&gt;
&lt;th&gt;ops-ceph-comp2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 vCPU&lt;/td&gt;
&lt;td&gt;6 vCPU&lt;/td&gt;
&lt;td&gt;6 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50 GB &lt;code&gt;/dev/sda&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50 GB &lt;code&gt;/dev/sda&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50 GB &lt;code&gt;/dev/sda&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk OSD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3x 25 GB (&lt;code&gt;/dev/sdb&lt;/code&gt;,&lt;code&gt;sdc&lt;/code&gt;,&lt;code&gt;sdd&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;3x 25 GB&lt;/td&gt;
&lt;td&gt;3x 25 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NIC 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ens18&lt;/code&gt; → 192.168.205.101/24&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ens18&lt;/code&gt; → 192.168.205.102/24&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ens18&lt;/code&gt; → 192.168.205.103/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NIC 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ens19&lt;/code&gt; → no IP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ens19&lt;/code&gt; → no IP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ens19&lt;/code&gt; → no IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ubuntu Noble 24.04&lt;/td&gt;
&lt;td&gt;Ubuntu Noble 24.04&lt;/td&gt;
&lt;td&gt;Ubuntu Noble 24.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ceph Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MON, MGR, OSD&lt;/td&gt;
&lt;td&gt;OSD&lt;/td&gt;
&lt;td&gt;OSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenStack Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Controller, Network&lt;/td&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Node Preparation&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Run the following steps on &lt;strong&gt;all nodes&lt;/strong&gt; unless stated otherwise.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.1 Set Hostname&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# On ops-ceph-ctrl
hostnamectl set-hostname ops-ceph-ctrl

# On ops-ceph-comp1
hostnamectl set-hostname ops-ceph-comp1

# On ops-ceph-comp2
hostnamectl set-hostname ops-ceph-comp2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 Configure /etc/hosts&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/hosts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add on all nodes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.205.101  ops-ceph-ctrl
192.168.205.102  ops-ceph-comp1
192.168.205.103  ops-ceph-comp2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 Configure Netplan&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/netplan/50-cloud-init.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example for &lt;strong&gt;ops-ceph-ctrl&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;network:
  version: 2
  renderer: networkd
  ethernets:
    ens18:
      addresses:
        - 192.168.205.101/24
      routes:
        - to: default
          via: 192.168.205.1
      nameservers:
        addresses: [8.8.8.8, 1.1.1.1]
    ens19:
      dhcp4: false
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Adjust IP for comp1 (&lt;code&gt;192.168.205.102&lt;/code&gt;) and comp2 (&lt;code&gt;192.168.205.103&lt;/code&gt;). &lt;code&gt;ens19&lt;/code&gt; only needs &lt;code&gt;dhcp4: false&lt;/code&gt; without an IP — Neutron will use it as the provider interface.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo netplan apply
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 Install Dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install -y git curl wget python3-dev python3-venv \
  libffi-dev gcc libssl-dev libdbus-glib-1-dev chrony sshpass nano
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 Configure Passwordless SSH (from ops-ceph-ctrl)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Generate SSH key (ed25519)
ssh-keygen -t ed25519 -C &quot;ops-ceph-ctrl&quot;

# Distribute public key to all nodes including self
ssh-copy-id opsce@ops-ceph-ctrl
ssh-copy-id opsce@ops-ceph-comp1
ssh-copy-id opsce@ops-ceph-comp2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6 Set NOPASSWD sudo (all nodes)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;opsce ALL=(ALL) NOPASSWD: ALL&quot; | sudo tee /etc/sudoers.d/opsce
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.7 Disable Swap &amp;amp; Time Sync&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo swapoff -a
sudo sed -i &apos;/swap/d&apos; /etc/fstab

sudo timedatectl set-timezone Asia/Jakarta
sudo systemctl enable --now chrony
chronyc tracking
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.8 Sysctl Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/sysctl.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.9 Install Docker&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -sL https://get.docker.com | bash
sudo systemctl enable --now docker
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Deploy Ceph Tentacle with cephadm&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;All steps are executed &lt;strong&gt;from ops-ceph-ctrl&lt;/strong&gt; unless stated otherwise.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.1 Install cephadm&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl --silent --remote-name --location \
  https://download.ceph.com/rpm-tentacle/el9/noarch/cephadm

chmod +x cephadm
sudo install -m 0755 cephadm /usr/local/sbin/cephadm

sudo cephadm add-repo --release tentacle
sudo apt update
sudo apt install -y cephadm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 Bootstrap Ceph Cluster&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm bootstrap \
  --mon-ip &amp;lt;your-controller-ip&amp;gt; \
  --cluster-network &amp;lt;your-subnet&amp;gt;/24 \
  --initial-dashboard-user &amp;lt;dashboard-user&amp;gt; \
  --initial-dashboard-password &amp;lt;dashboard-password&amp;gt; \
  --allow-overwrite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- ceph -s
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 Install ceph-common&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# On ctrl
sudo cephadm install ceph-common

# On comp1 and comp2
ssh -t ops-ceph-comp1 &quot;sudo cephadm add-repo --release tentacle &amp;amp;&amp;amp; sudo apt update &amp;amp;&amp;amp; sudo apt install -y ceph-common&quot;
ssh -t ops-ceph-comp2 &quot;sudo cephadm add-repo --release tentacle &amp;amp;&amp;amp; sudo apt update &amp;amp;&amp;amp; sudo apt install -y ceph-common&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 Copy cephadm SSH Key to All Nodes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- ceph cephadm get-pub-key | \
  sudo tee /etc/ceph/ceph.pub

# Must run as root
sudo su
ssh-copy-id -f -i /etc/ceph/ceph.pub root@ops-ceph-comp1
ssh-copy-id -f -i /etc/ceph/ceph.pub root@ops-ceph-comp2
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.5 Add Nodes to Cluster&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- ceph orch host add ops-ceph-comp1 192.168.205.102
sudo cephadm shell -- ceph orch host add ops-ceph-comp2 192.168.205.103

# Verify
sudo cephadm shell -- ceph orch host ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.6 Deploy OSD&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# ops-ceph-ctrl
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-ctrl:/dev/sdb
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-ctrl:/dev/sdc
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-ctrl:/dev/sdd

# ops-ceph-comp1
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-comp1:/dev/sdb
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-comp1:/dev/sdc
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-comp1:/dev/sdd

# ops-ceph-comp2
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-comp2:/dev/sdb
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-comp2:/dev/sdc
sudo cephadm shell -- ceph orch daemon add osd ops-ceph-comp2:/dev/sdd
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Use &lt;code&gt;hostname:/dev/sdX&lt;/code&gt; format — not &lt;code&gt;user@hostname:/dev/sdX&lt;/code&gt;. The latter causes a &quot;host not found&quot; error. Also, warnings like &lt;code&gt;Not using image... not in the list of non-dangling images&lt;/code&gt; are cosmetic and safe to ignore.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.7 Verify Cluster&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- ceph -s
sudo cephadm shell -- ceph osd tree
sudo cephadm shell -- ceph df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure all 9 OSDs (3 nodes × 3 disks) are &lt;code&gt;up&lt;/code&gt; and &lt;code&gt;in&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s what the Ceph Block Images dashboard looks like after a successful deployment:&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/blog/evidence/ceph.png&quot; alt=&quot;Ceph Block Images Dashboard&quot; style=&quot;max-width: 100%; border-radius: 8px; margin: 1rem 0;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;And the Grafana monitoring dashboard showing cluster health, OSD performance, and resource utilization:&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/blog/evidence/ceph-grafana.png&quot; alt=&quot;Ceph Grafana Monitoring Dashboard&quot; style=&quot;max-width: 100%; border-radius: 8px; margin: 1rem 0;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Ceph Pool &amp;amp; Keyring Setup for OpenStack&lt;/h2&gt;
&lt;h3&gt;4.1 Create Pools&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- bash -c &quot;
  ceph osd pool create volumes
  ceph osd pool create images
  ceph osd pool create backups
  ceph osd pool create vms

  rbd pool init volumes
  rbd pool init images
  rbd pool init backups
  rbd pool init vms
&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 Create Keyrings&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- bash -c &quot;
  ceph auth get-or-create client.glance \
    mon &apos;allow r&apos; \
    osd &apos;allow class-read object_prefix rbd_children, allow rwx pool=images&apos; \
    -o /etc/ceph/ceph.client.glance.keyring

  ceph auth get-or-create client.cinder \
    mon &apos;allow r&apos; \
    osd &apos;allow class-read object_prefix rbd_children, allow rwx pool=volumes, allow rwx pool=images&apos; \
    -o /etc/ceph/ceph.client.cinder.keyring

  ceph auth get-or-create client.nova \
    mon &apos;allow r&apos; \
    osd &apos;allow class-read object_prefix rbd_children, allow rwx pool=vms, allow rx pool=images&apos; \
    -o /etc/ceph/ceph.client.nova.keyring

  ceph auth get-or-create client.cinder-backup \
    mon &apos;allow r&apos; \
    osd &apos;allow class-read object_prefix rbd_children, allow rwx pool=backups&apos; \
    -o /etc/ceph/ceph.client.cinder-backup.keyring
&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 Export Keyrings to Host&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- ceph auth get client.glance | sudo tee /etc/ceph/ceph.client.glance.keyring
sudo cephadm shell -- ceph auth get client.cinder | sudo tee /etc/ceph/ceph.client.cinder.keyring
sudo cephadm shell -- ceph auth get client.nova | sudo tee /etc/ceph/ceph.client.nova.keyring
sudo cephadm shell -- ceph auth get client.cinder-backup | sudo tee /etc/ceph/ceph.client.cinder-backup.keyring

sudo chmod 640 /etc/ceph/ceph.client.*.keyring
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.4 Generate ceph.conf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo cephadm shell -- ceph config generate-minimal-conf | sudo tee /etc/ceph/ceph.conf

# Remove leading tabs to prevent kolla-ansible issues
sudo sed -i &apos;s/^\t//&apos; /etc/ceph/ceph.conf

cat /etc/ceph/ceph.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Install Kolla-Ansible&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Run from &lt;strong&gt;ops-ceph-ctrl&lt;/strong&gt; as user &lt;code&gt;opsce&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5.1 Create Virtual Environment&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;python3 -m venv ~/kolla-venv
source ~/kolla-venv/bin/activate

# Auto-activate on login
echo &apos;source ~/kolla-venv/bin/activate&apos; &amp;gt;&amp;gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 Install Kolla-Ansible&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install -U pip
pip install &apos;ansible-core&amp;gt;=2.16,&amp;lt;2.17&apos;
pip install docker dbus-python

# Install kolla-ansible from master
pip install git+https://opendev.org/openstack/kolla-ansible@master
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.3 Setup Kolla Directories&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/kolla
sudo chown $USER:$USER /etc/kolla

cp -r ~/kolla-venv/share/kolla-ansible/etc_examples/kolla/* /etc/kolla/
cp ~/kolla-venv/share/kolla-ansible/ansible/inventory/multinode ~/multinode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.4 Install Ansible Galaxy Dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kolla-ansible install-deps
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Kolla-Ansible Configuration&lt;/h2&gt;
&lt;h3&gt;6.1 Ansible Config&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/ansible
sudo tee /etc/ansible/ansible.cfg &amp;lt;&amp;lt;EOF
[defaults]
host_key_checking = False
pipelining = True
forks = 100
interpreter_python = /usr/bin/python3
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 Multinode Inventory&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nano ~/multinode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the top section:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[control]
ops-ceph-ctrl

[network]
ops-ceph-ctrl

[compute]
ops-ceph-comp1
ops-ceph-comp2

[monitoring]
ops-ceph-ctrl

[storage]
ops-ceph-ctrl
ops-ceph-comp1
ops-ceph-comp2

[deployment]
localhost       ansible_connection=local
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Leave all &lt;code&gt;[xxx:children]&lt;/code&gt; groups as default from the template.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible -i ~/multinode all -m ping
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.3 Generate Passwords&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kolla-genpwd

# Set admin password (replace with your own secure password)
sed -i &apos;s/^keystone_admin_password:.*/keystone_admin_password: &amp;lt;your-password&amp;gt;/&apos; /etc/kolla/passwords.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4 Configure globals.yml&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nano /etc/kolla/globals.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# =====================
# BASE IMAGE
# =====================
kolla_base_distro: &quot;ubuntu&quot;

# =====================
# NETWORKING
# =====================
network_interface: &quot;ens18&quot;
neutron_external_interface: &quot;ens19&quot;
kolla_internal_vip_address: &quot;192.168.205.100&quot;

# =====================
# ENABLE SERVICES
# =====================
enable_openstack_core: &quot;yes&quot;
enable_cinder: &quot;yes&quot;
enable_cinder_backup: &quot;yes&quot;

# =====================
# CEPH INTEGRATION
# =====================
glance_backend_ceph: &quot;yes&quot;
cinder_backend_ceph: &quot;yes&quot;
nova_backend_ceph: &quot;yes&quot;

ceph_glance_user: &quot;glance&quot;
ceph_glance_pool_name: &quot;images&quot;
ceph_cinder_user: &quot;cinder&quot;
ceph_cinder_pool_name: &quot;volumes&quot;
ceph_cinder_backup_user: &quot;cinder-backup&quot;
ceph_cinder_backup_pool_name: &quot;backups&quot;
ceph_nova_user: &quot;nova&quot;
ceph_nova_pool_name: &quot;vms&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;kolla_internal_vip_address&lt;/code&gt; must be an unused IP in the &lt;code&gt;ens18&lt;/code&gt; subnet. This IP will automatically appear on the &lt;code&gt;ens18&lt;/code&gt; interface of ops-ceph-ctrl after deployment as the HAProxy VIP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;7. External Ceph Integration with Kolla-Ansible&lt;/h2&gt;
&lt;h3&gt;7.1 Create Directories&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/kolla/config/glance
mkdir -p /etc/kolla/config/nova
mkdir -p /etc/kolla/config/cinder/cinder-volume
mkdir -p /etc/kolla/config/cinder/cinder-backup
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.2 Copy ceph.conf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cp /etc/ceph/ceph.conf /etc/kolla/config/glance/ceph.conf
cp /etc/ceph/ceph.conf /etc/kolla/config/nova/ceph.conf
cp /etc/ceph/ceph.conf /etc/kolla/config/cinder/ceph.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.3 Copy Keyrings&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Glance
cp /etc/ceph/ceph.client.glance.keyring \
   /etc/kolla/config/glance/ceph.client.glance.keyring

# Nova (needs both nova AND cinder keyrings)
sudo cp /etc/ceph/ceph.client.nova.keyring \
   /etc/kolla/config/nova/ceph.client.nova.keyring
sudo cp /etc/ceph/ceph.client.cinder.keyring \
   /etc/kolla/config/nova/ceph.client.cinder.keyring

# Cinder Volume
sudo cp /etc/ceph/ceph.client.cinder.keyring \
   /etc/kolla/config/cinder/cinder-volume/ceph.client.cinder.keyring

# Cinder Backup
sudo cp /etc/ceph/ceph.client.cinder.keyring \
   /etc/kolla/config/cinder/cinder-backup/ceph.client.cinder.keyring
sudo cp /etc/ceph/ceph.client.cinder-backup.keyring \
   /etc/kolla/config/cinder/cinder-backup/ceph.client.cinder-backup.keyring
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.4 Fix Ownership &amp;amp; Permissions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo chown -R opsce:opsce /etc/kolla/config
sudo chmod 640 /etc/kolla/config/cinder/cinder-backup/*.keyring
sudo chmod 640 /etc/kolla/config/cinder/cinder-volume/*.keyring
sudo chmod 640 /etc/kolla/config/nova/*.keyring
sudo chmod 640 /etc/kolla/config/glance/*.keyring
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7.5 Verify File Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;find /etc/kolla/config -type f -printf &quot;%u %p\n&quot; | sort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output (all owned by &lt;code&gt;opsce&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opsce /etc/kolla/config/cinder/ceph.conf
opsce /etc/kolla/config/cinder/cinder-backup/ceph.client.cinder-backup.keyring
opsce /etc/kolla/config/cinder/cinder-backup/ceph.client.cinder.keyring
opsce /etc/kolla/config/cinder/cinder-volume/ceph.client.cinder.keyring
opsce /etc/kolla/config/glance/ceph.conf
opsce /etc/kolla/config/glance/ceph.client.glance.keyring
opsce /etc/kolla/config/nova/ceph.conf
opsce /etc/kolla/config/nova/ceph.client.cinder.keyring
opsce /etc/kolla/config/nova/ceph.client.nova.keyring
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Deploy OpenStack&lt;/h2&gt;
&lt;h3&gt;8.1 Bootstrap Servers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kolla-ansible bootstrap-servers -i ~/multinode
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; It&apos;s common for Ceph MON/MGR services to go &lt;code&gt;inactive dead&lt;/code&gt; after this step, since kolla-ansible reconfigures Docker daemon settings. If this happens, a simple &lt;code&gt;sudo reboot&lt;/code&gt; on affected nodes is the cleanest fix. After reboot, verify Ceph health with &lt;code&gt;sudo ceph -s&lt;/code&gt; before proceeding.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;8.2 Pre-checks&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kolla-ansible prechecks -i ~/multinode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.3 Deploy&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kolla-ansible deploy -i ~/multinode
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Expect this to take &lt;strong&gt;20-60 minutes&lt;/strong&gt; depending on your hardware and network speed. Occasional single-task failures on compute nodes are typically timing-related and generally resolve themselves — verify with &lt;code&gt;openstack compute service list&lt;/code&gt; after deployment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;8.4 Post-Deploy&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kolla-ansible post-deploy -i ~/multinode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.5 Install OpenStack Client &amp;amp; Load Credentials&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install python-openstackclient \
  -c https://releases.openstack.org/constraints/upper/2025.2

source /etc/kolla/admin-openrc.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.6 Verify Services&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack service list
openstack compute service list
openstack network agent list
openstack volume service list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All services should be &lt;code&gt;up&lt;/code&gt;. Note that &lt;code&gt;cinder-volume&lt;/code&gt; backend shows &lt;code&gt;rbd-1&lt;/code&gt;, confirming successful Ceph integration.&lt;/p&gt;
&lt;p&gt;Here&apos;s the actual output from a working deployment — all services, network agents, compute services, and volume services are confirmed &lt;code&gt;up&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/blog/evidence/list-service-openstack.png&quot; alt=&quot;OpenStack Service List&quot; style=&quot;max-width: 100%; border-radius: 8px; margin: 1rem 0;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. OpenStack Network Setup&lt;/h2&gt;
&lt;h3&gt;9.1 Create Provider Network (External)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack network create \
  --share \
  --external \
  --provider-physical-network physnet1 \
  --provider-network-type flat \
  public-net

openstack subnet create \
  --network public-net \
  --subnet-range 17.19.21.0/24 \
  --gateway 17.19.21.254 \
  --allocation-pool start=17.19.21.100,end=17.19.21.200 \
  --dns-nameserver 8.8.8.8 \
  public-subnet
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.2 Create Private/Tenant Network&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack network create private-net

openstack subnet create \
  --network private-net \
  --subnet-range 10.0.0.0/24 \
  --gateway 10.0.0.1 \
  --dns-nameserver 8.8.8.8 \
  private-subnet
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.3 Create Router&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack router create main-router
openstack router set main-router --external-gateway public-net
openstack router add subnet main-router private-subnet

# Verify
openstack router show main-router
openstack port list --router main-router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the router is created, you can visualize the network topology in Horizon:&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/blog/evidence/network-openstack.png&quot; alt=&quot;OpenStack Network Topology&quot; style=&quot;max-width: 100%; border-radius: 8px; margin: 1rem 0;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;9.4 Create Security Group&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack security group create security-group-net \
  --description &apos;Allow SSH and ICMP&apos;

openstack security group rule create \
  --protocol icmp security-group-net

openstack security group rule create \
  --protocol tcp --ingress --dst-port 22 security-group-net

openstack security group rule list security-group-net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.5 Create Floating IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack floating ip create public-net
openstack floating ip list
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Floating IPs are only externally reachable if the provider network interface is bridged to your physical network. In isolated lab environments, you can still access VMs via their private IPs using the Neutron router namespace (see Section 11).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;10. Create VM Instance&lt;/h2&gt;
&lt;h3&gt;10.1 Upload Image&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget http://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img

openstack image create \
  --disk-format qcow2 \
  --container-format bare \
  --public \
  --file ./cirros-0.6.2-x86_64-disk.img \
  cirros-0.6.2

openstack image list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10.2 Create Keypair&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack keypair create \
  --public-key ~/.ssh/id_ed25519.pub \
  controller-key

openstack keypair list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10.3 Create Flavor&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openstack flavor create \
  --ram 512 \
  --disk 8 \
  --vcpus 1 \
  --public \
  c1-small

openstack flavor list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;10.4 Create Instance&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# First instance
openstack server create \
  --flavor c1-small \
  --image cirros-0.6.2 \
  --key-name controller-key \
  --security-group security-group-net \
  --network private-net \
  cirros-instance-test

# Second instance (for inter-VM connectivity test)
openstack server create \
  --flavor c1-small \
  --image cirros-0.6.2 \
  --key-name controller-key \
  --security-group security-group-net \
  --network private-net \
  cirros-instance-test-2

# Check status
openstack server list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait until status is &lt;code&gt;ACTIVE&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;10.5 Assign Floating IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# List available floating IPs
openstack floating ip list

# Assign to each instance
openstack server add floating ip cirros-instance-test &amp;lt;floating-ip-1&amp;gt;
openstack server add floating ip cirros-instance-test-2 &amp;lt;floating-ip-2&amp;gt;

# Verify — instances should have 2 IPs (private + floating)
openstack server list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s what it looks like in Horizon — both instances are &lt;code&gt;Active&lt;/code&gt; and &lt;code&gt;Running&lt;/code&gt; with their private and floating IPs assigned:&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/blog/evidence/openstack.png&quot; alt=&quot;OpenStack Instances in Horizon&quot; style=&quot;max-width: 100%; border-radius: 8px; margin: 1rem 0;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;And the detailed server list from the CLI, confirming Ceph-backed storage and network connectivity:&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;/images/blog/evidence/server-list.png&quot; alt=&quot;OpenStack Server List and Details&quot; style=&quot;max-width: 100%; border-radius: 8px; margin: 1rem 0;&quot; /&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;11. Verify VM Connectivity&lt;/h2&gt;
&lt;h3&gt;11.1 Check Router Namespace&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# List all namespaces
sudo ip netns list
# Output: qrouter-xxx (id: N), qdhcp-xxx (id: N)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11.2 Ping VM from Namespace&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Replace &amp;lt;router-id&amp;gt; with the ID from ip netns list output
sudo ip netns exec qrouter-&amp;lt;router-id&amp;gt; ping -c 4 10.0.0.71
sudo ip netns exec qrouter-&amp;lt;router-id&amp;gt; ping -c 4 10.0.0.104
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11.3 SSH to VM from Namespace&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# SSH using key (recommended)
sudo ip netns exec qrouter-&amp;lt;router-id&amp;gt; ssh -i ~/.ssh/id_ed25519 cirros@10.0.0.71

# Or SSH with password (cirros password: gocubsgo)
sudo ip netns exec qrouter-&amp;lt;router-id&amp;gt; ssh cirros@10.0.0.71
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11.4 Test Inter-VM Connectivity&lt;/h3&gt;
&lt;p&gt;From inside cirros-instance-test, ping the other VM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Enter the first VM
sudo ip netns exec qrouter-&amp;lt;router-id&amp;gt; ssh cirros@10.0.0.71

# From inside the VM, ping the second VM
ping 10.0.0.104
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You cannot ping VMs directly from the controller host — there&apos;s no route to the tenant or provider networks from the host&apos;s default namespace. Always use &lt;code&gt;sudo ip netns exec qrouter-&amp;lt;id&amp;gt;&lt;/code&gt; to access VM networks from the controller.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;11.5 Verify OVS Bridge&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo docker exec openvswitch_vswitchd ovs-vsctl show
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;br-ex&lt;/code&gt; has port &lt;code&gt;ens19&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;br-int&lt;/code&gt; has &lt;code&gt;qr-xxx&lt;/code&gt; (router port) and &lt;code&gt;tap-xxx&lt;/code&gt; (VM port)&lt;/li&gt;
&lt;li&gt;All bridge controllers show &lt;code&gt;is_connected: true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;MON/MGR dies after bootstrap-servers&lt;/h3&gt;
&lt;p&gt;kolla-ansible reconfigures Docker settings which can prevent Ceph containers from restarting. A &lt;code&gt;sudo reboot&lt;/code&gt; is the cleanest fix. Alternatively, manually start the services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start ceph-&amp;lt;fsid&amp;gt;@mon.&amp;lt;hostname&amp;gt;.service
sudo systemctl start ceph-&amp;lt;fsid&amp;gt;@mgr.&amp;lt;hostname&amp;gt;.&amp;lt;id&amp;gt;.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Error &quot;host not found&quot; when adding OSD&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;hostname:/dev/sdX&lt;/code&gt; format — &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;user@hostname:/dev/sdX&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# CORRECT
sudo cephadm shell -- ceph orch daemon add osd &amp;lt;hostname&amp;gt;:/dev/sdb
# WRONG
sudo cephadm shell -- ceph orch daemon add osd user@&amp;lt;hostname&amp;gt;:/dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Warning &quot;Not using image... non-dangling images&quot;&lt;/h3&gt;
&lt;p&gt;Cosmetic warning, not an error. Safe to ignore.&lt;/p&gt;
&lt;h3&gt;kolla-ansible: command not found&lt;/h3&gt;
&lt;p&gt;Ensure the Python virtual environment is activated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source ~/kolla-venv/bin/activate
which kolla-ansible
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Missing sudo password during bootstrap-servers&lt;/h3&gt;
&lt;p&gt;Ensure passwordless sudo is configured for your deploy user:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;&amp;lt;user&amp;gt; ALL=(ALL) NOPASSWD: ALL&quot; | sudo tee /etc/sudoers.d/&amp;lt;user&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ceph.conf with leading tabs&lt;/h3&gt;
&lt;p&gt;Kolla-ansible may fail to parse ceph.conf if it contains tabs. Strip them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sed -i &apos;s/^\t//&apos; /etc/kolla/config/*/ceph.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cannot ping VM directly from controller&lt;/h3&gt;
&lt;p&gt;This is expected — the host has no route to tenant/provider networks. Use the Neutron router namespace:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip netns list
sudo ip netns exec qrouter-&amp;lt;id&amp;gt; ping -c 4 &amp;lt;vm-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ceph.com/en/tentacle/&quot;&gt;Ceph Tentacle Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ceph.com/en/tentacle/cephadm/install/&quot;&gt;cephadm Install Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.openstack.org/kolla-ansible/latest/reference/storage/external-ceph-guide.html&quot;&gt;Kolla-Ansible External Ceph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.openstack.org/kolla-ansible/latest/user/quickstart.html&quot;&gt;Kolla-Ansible Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This guide is based on a real-world deployment. Adjust IPs, hostnames, credentials, and disk paths to match your own environment.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>