<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ana Villar</title>
    <description>The latest articles on DEV Community by Ana Villar (@vilan011).</description>
    <link>https://dev.to/vilan011</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960129%2F478a841c-62df-4927-9b8e-38ef310de9dd.png</url>
      <title>DEV Community: Ana Villar</title>
      <link>https://dev.to/vilan011</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vilan011"/>
    <language>en</language>
    <item>
      <title>Building an OpenShift 4.18 Cluster from Scratch: Part 3 - The Deployment Lifecycle &amp; Troubleshooting</title>
      <dc:creator>Ana Villar</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:25:55 +0000</pubDate>
      <link>https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-3-the-deployment-lifecycle-troubleshooting-4404</link>
      <guid>https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-3-the-deployment-lifecycle-troubleshooting-4404</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Note: This is Part 3 of a multi-part series.&lt;/p&gt;

&lt;p&gt;✅ [&lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-1-the-network-foundation-utilities-server-5g92"&gt;Part 1: The Network Foundation &amp;amp; Utilities Server&lt;/a&gt;] (Published)&lt;br&gt;
✅ [&lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-2-generating-ignition-configs-vm-prep-4ka9"&gt;Part 2: Generating Ignition Configs &amp;amp; VM Prep&lt;/a&gt;] (Published)&lt;br&gt;
&lt;a href="https://dev.toComing%20Soon"&gt;Part 4: Post-Install Hardening &amp;amp; User Management&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Parts 1 and 2, we built the foundation and prepared our Ignition configurations. The utilities server was humming, DHCP and DNS were routing requests correctly, and our VMs were ready to PXE boot. Now comes the moment of truth: actually deploying the cluster.&lt;/p&gt;

&lt;p&gt;But here's the thing about bare metal OpenShift deployments—they rarely go perfectly on the first try. In this part, I'll walk through exactly what happened when my first deployment attempt failed, how I recovered, and the critical troubleshooting steps every OpenShift administrator should know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review we have all in place
&lt;/h2&gt;

&lt;p&gt;At this point I want you to check that everything is ready to go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have created 7 virtual machines, with the specifications indicated in &lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-1-the-network-foundation-utilities-server-5g92"&gt;Part 1&lt;/a&gt;, under &lt;strong&gt;Lab Environment Overview section&lt;/strong&gt;. &lt;/li&gt;
&lt;li&gt;The VMs are called bootstos, masteros01, masteros02, masteros03, workeros01, workeros02 and workeros03. &lt;/li&gt;
&lt;li&gt;Those have been created with &lt;strong&gt;UEFI&lt;/strong&gt; (or EFI) firmware.&lt;/li&gt;
&lt;li&gt;You have disabled Secured Boot, as explained in &lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-2-generating-ignition-configs-vm-prep-4ka9"&gt;Part 2&lt;/a&gt;, under the section &lt;strong&gt;Disabling Secure Boot on VMs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;And you have noted the MAC Address of the NIC attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, ensure you have updated the Kea configuration file (&lt;strong&gt;/etc/kea/kea-dhcp4.conf&lt;/strong&gt;) in your utilities server VM with the correct MAC address (hw-address), under reservations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"reservations": [
          { "hw-address": "AA:BB:CC:DD:EE:01", "ip-address": "192.168.110.100", "hostname": "bootstos.oc41827.internal.local" },

          { "hw-address": "AA:BB:CC:DD:EE:02", "ip-address": "192.168.110.101", "hostname": "masteros01.oc41827.internal.local" },
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have to update the file now, check the syntax using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kea-dhcp4 &lt;span class="nt"&gt;-t&lt;/span&gt; /etc/kea/kea-dhcp4.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and restart the service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart kea-dhcp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Boot the 7 virtual machines
&lt;/h3&gt;

&lt;p&gt;Using your Cockpit interface or with the command &lt;code&gt;sudo virsh start&lt;/code&gt; (or, if using a different hypervisor, whatever CLI or GUI interface you prefer), start the virtual machines.&lt;/p&gt;

&lt;p&gt;Start first with &lt;strong&gt;bootstos&lt;/strong&gt;. When the iPXE menu appears, ensure you select the menu entry called &lt;strong&gt;Install OpenShift 4.18.27 Bootstrap&lt;/strong&gt;. In a few moments, the process will complete and it will automatically reboot. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;If, for whatever reason, you had to manually force the VM to boot from network, once the installation is completed, shutdown the VM and change the boot order to boot from its local disk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Do the same for the other six virtual machines. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;masteros01&lt;/strong&gt;, &lt;strong&gt;masteros02&lt;/strong&gt; and &lt;strong&gt;masteros03&lt;/strong&gt;, when the iPXE menu appears, select &lt;strong&gt;Install OpenShift 4.18.27 Master Node&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;workeros01&lt;/strong&gt;, &lt;strong&gt;workeros02&lt;/strong&gt; and &lt;strong&gt;workeros03&lt;/strong&gt;, when the iPXE menu appears, select &lt;strong&gt;Install OpenShift 4.18.27 Worker Node&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Launch and monitor the progress
&lt;/h3&gt;

&lt;p&gt;Now that the 7 VMs are up and running, open two SSH sessions to your utilities server, one for the actual deployment and the second to be able to monitor the progress.&lt;/p&gt;

&lt;p&gt;In the first session, start the deployment running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;openshift-install &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./oc41827upi wait-for bootstrap-complete &lt;span class="nt"&gt;--log-level&lt;/span&gt; debug 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the second session, connect to the bootstrap VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@bootstos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and once there, monitor the deployment from here as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; release-image.service &lt;span class="nt"&gt;-u&lt;/span&gt; bootkube.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  🚨 Certificate Expiration
&lt;/h4&gt;

&lt;p&gt;If you followed the steps the ignition files (getting the pull secret and updating the install-config.yaml configuration file, as indicated in the section &lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-2-generating-ignition-configs-vm-prep-4ka9"&gt;Generating installation configuration file&lt;/a&gt;) a few days back, you are most likely to see the deployment failing after a few minutes, and errors similar to those in the bootstrap ssh session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;May 01 13:34:49 bootstos.oc41827.internal.local bootkube.sh[4018]: [#&lt;/span&gt;3964] failed to fetch discovery: Get &lt;span class="s2"&gt;"https://localhost:6443/api?timeout=32s"&lt;/span&gt;: dial tcp &lt;span class="o"&gt;[&lt;/span&gt;::1]:6443: connect: connection refused
&lt;span class="gp"&gt;May 01 13:34:50 bootstos.oc41827.internal.local cluster-bootstrap[4049]: [#&lt;/span&gt;3965] failed to fetch discovery: Get &lt;span class="s2"&gt;"https://localhost:6443/api?timeout=32s"&lt;/span&gt;: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current &lt;span class="nb"&gt;time &lt;/span&gt;2026-05-01T13:34:50Z is after 2026-04-27T18:15:56Z
&lt;span class="gp"&gt;May 01 13:34:50 bootstos.oc41827.internal.local bootkube.sh[4018]: [#&lt;/span&gt;3965] failed to fetch discovery: Get &lt;span class="s2"&gt;"https://localhost:6443/api?timeout=32s"&lt;/span&gt;: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current &lt;span class="nb"&gt;time &lt;/span&gt;2026-05-01T13:34:50Z is after 2026-04-27T18:15:56Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What Happened?&lt;/strong&gt; OpenShift certificates generated during the install-config.yaml creation have a limited validity window (typically 24 hours). My first deployment attempt came four days after configuration generation, causing the certificates to expire before installation began.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; Regenerate Everything, I am afraid.&lt;/p&gt;

&lt;p&gt;As seen in &lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-2-generating-ignition-configs-vm-prep-4ka9"&gt;Part 2&lt;/a&gt;, those are the steps needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get a fresh pull secret from console.redhat.com&lt;/li&gt;
&lt;li&gt;Move old config fiels aside&lt;/li&gt;
&lt;li&gt;Recreate the configuration directory:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;oc41827upi oc41827upi.old
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;oc41827upi
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;oc41827upi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Copy the fresh pull secret** and format it properly:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool pull-secret &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pull-secret.json
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;pull-secret.json | jq &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/oc41827upi/pull-secret-online.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Update install-config.yaml
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt; ~/oc41827upi/install-config.yaml
apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 3
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
metadata:
  name: oc41827
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  networkType: OVNKubernetes
  serviceNetwork:
  - 172.30.0.0/16
platform:
  none: {}
fips: false
pullSecret: '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/oc41827upi/pull-secret.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'
sshKey: '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/oc41827upi.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Regenerate manifests and Ignition files:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/oc41827upi
&lt;span class="nv"&gt;$ &lt;/span&gt;openshift-install create manifests &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;openshift-install create ignition-configs &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Copy new Ignition files to Apache:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.ign /var/www/html/oc41827/ignition/
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +r /var/www/html/oc41827/ignition/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;restorecon &lt;span class="nt"&gt;-Rv&lt;/span&gt; /var/www/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;With fresh configurations, reset the VMs:&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Shut down all nodes&lt;/li&gt;
&lt;li&gt;Delete their virtual disks and attach a new one (to force clean installs)&lt;/li&gt;
&lt;li&gt;Boot via iPXE menu selection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💡 &lt;strong&gt;Key Lesson:&lt;/strong&gt; Keep your configuration workflow tight. Don't generate Ignition files until you're ready to deploy immediately afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bootstrap Complete: Removing the Temporary Node
&lt;/h3&gt;

&lt;p&gt;If all goes well, you can expect outputs similar to this on your utilities ssh session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openshift-install &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./oc41827upi wait-for bootstrap-complete &lt;span class="nt"&gt;--log-level&lt;/span&gt; debug
&lt;span class="go"&gt;DEBUG OpenShift Installer 4.18.37  
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;INFO Bootstrap etcd member has been removed       
INFO It is now safe to remove the bootstrap resources 
DEBUG Time elapsed per stage:                      
DEBUG Bootstrap Complete: 26m48s                   
INFO Time elapsed: 26m48s                         
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and this on the bootstrap session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[core@bootstos ~]$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; release-image.service &lt;span class="nt"&gt;-u&lt;/span&gt; bootkube.service
&lt;span class="go"&gt;May 01 13:55:00 bootstos.oc41827.internal.local cluster-bootstrap[4041]: "99_openshift-machineconfig_99-master-ssh.yaml": failed to create machineconfigs.v1.machineconfiguration.openshift.io/99-master-ssh -n : the server could not find the requested resource
May 01 13:55:00 bootstos.oc41827.internal.local cluster-bootstrap[4041]: "99_openshift-machineconfig_99-worker-ssh.yaml": failed to create machineconfigs.v1.machineconfiguration.openshift.io/99-worker-ssh -n : the server could not find the requested resource
May 01 13:55:00 bootstos.oc41827.internal.local cluster-bootstrap[4041]: Created "0000_00_cluster-version-operator_01_adminack_configmap.yaml" configmaps.v1./admin-acks -n openshift-config
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;May 01 14:23:13 bootstos.oc41827.internal.local bootkube.sh[5815]: Success while trying to reach API_INT_URL's https endpoint at https://api-int.oc41827.internal.local:6443/readyz
May 01 14:23:13 bootstos.oc41827.internal.local bootkube.sh[5815]: bootkube.service complete
May 01 14:23:13 bootstos.oc41827.internal.local systemd[1]: bootkube.service: Deactivated successfully.
May 01 14:23:13 bootstos.oc41827.internal.local systemd[1]: bootkube.service: Consumed 4.396s CPU time.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This typically takes &lt;strong&gt;25-40 minutes&lt;/strong&gt; depending on your hardware and network speed.&lt;/p&gt;

&lt;p&gt;Once &lt;em&gt;bootstrap-complete&lt;/em&gt; succeeds, OpenShift automatically removes the bootstrap etcd member. Now we must manually remove the bootstrap node from our HAProxy load balancer:&lt;/p&gt;

&lt;h4&gt;
  
  
  Edit HAProxy Configuration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/haproxy/haproxy.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comment out or remove the bootstrap server entries from both the API server and machine config server listen sections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# round robin balancing for RHOCP Kubernetes API Server
#server bootstos bootstos.oc41827.internal.local:6443 verify none check check-ssl inter 10s fall 2 rise 3 backup

# round robin balancing for RHOCP Machine Config Server
#server bootstos bootstos.oc41827.internal.local:22623 check inter 1s backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart HAProxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart haproxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Shutdown bootstrap
&lt;/h4&gt;

&lt;p&gt;Now you can shutdown the bootstrap machine. Using the session you opened to monitor the progress, press Crtl+C to finish the journalctl command and  use the shutdown command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;core@bootstos ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;shutdown &lt;span class="nt"&gt;-h&lt;/span&gt; now
&lt;span class="o"&gt;[&lt;/span&gt;core@bootstos ~]&lt;span class="nv"&gt;$ &lt;/span&gt;Connection to bootstos closed by remote host.
Connection to bootstos closed.
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finish the deployment
&lt;/h2&gt;

&lt;p&gt;Check the status of the OpenShift cluster on your ssh session to the utilities server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/oc41827upi/auth/kubeconfig
&lt;span class="nv"&gt;$ &lt;/span&gt;oc &lt;span class="nb"&gt;whoami
&lt;/span&gt;system:admin
&lt;span class="nv"&gt;$ &lt;/span&gt;oc get nodes
NAME                                      STATUS   ROLES                  AGE   VERSION
masteros01.oc41827.internal.local   Ready    control-plane,master   32m   v1.31.14
masteros02.oc41827.internal.local   Ready    control-plane,master   30m   v1.31.14
masteros03.oc41827.internal.local   Ready    control-plane,master   30m   v1.31.14
&lt;span class="nv"&gt;$ &lt;/span&gt;oc get clusterversion
NAME      VERSION   AVAILABLE   PROGRESSING   SINCE   STATUS
version             False       True          36m     Unable to apply 4.18.37: some cluster operators are not available
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where are the worker nodes? Wait, we haven't finish yet, so let's run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;openshift-install &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./oc41827upi wait-for install-complete &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On another session, keep an eye on Certificate Signing Requests (CSRs) being pending for approval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/oc41827upi/auth/kubeconfig
&lt;span class="nv"&gt;$ &lt;/span&gt;oc get csr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might see an output similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                             AGE   SIGNERNAME                                    REQUESTOR                                                                         REQUESTEDDURATION   CONDITION
csr-2zppd                                        35m   kubernetes.io/kubelet-serving                 system:node:masteros02.oc41827.internal.local                                     &amp;lt;none&amp;gt;              Approved,Issued
...
csr-hnt8m                                        25m   kubernetes.io/kube-apiserver-client           system:node:masteros03.oc41827.internal.local                                     24h                 Approved,Issued
csr-lldhf                                        1s    kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         &amp;lt;none&amp;gt;              Pending
csr-lrphg                                        35m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         &amp;lt;none&amp;gt;              Approved,Issued
csr-pqbm8                                        25m   kubernetes.io/kube-apiserver-client           system:node:masteros02.oc41827.internal.local                                     24h                 Approved,Issued
csr-v8wtg                                        35m   kubernetes.io/kube-apiserver-client-kubelet   system:serviceaccount:openshift-machine-config-operator:node-bootstrapper         &amp;lt;none&amp;gt;              Approved,Issued
csr-xwtxx                                        26m   kubernetes.io/kube-apiserver-client           system:node:masteros02.oc41827.internal.local                                     24h                 Approved,Issued
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those pending CSR need to be approved. This is where many administrators get stuck. When nodes first join the cluster, they request certificates for communication with the API server. Until these are approved, nodes remain in &lt;code&gt;NotReady&lt;/code&gt; state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approving Certificate Signing Requests (CSRs)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Manual Approval&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Approve pending CSRs individually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;oc adm certificate approve csr-lldhf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Automated Batch Approval&lt;/strong&gt;&lt;br&gt;
For efficiency, especially when workers join, use this command to approve all pending CSRs at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;oc get csr &lt;span class="nt"&gt;-o&lt;/span&gt; go-template&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{range .items}}{{if not .status}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}'&lt;/span&gt; | xargs oc adm certificate approve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep reviewing the list of CSRs and keep approving until the deployment has finished.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output confirming deployment success
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openshift-install &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;oc41827upi wait-for install-complete &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debug
&lt;span class="go"&gt;DEBUG OpenShift Installer 4.18.37                  
DEBUG Built from commit 488926dc2c95d96460ee9939929a76ed23e1c596 
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;INFO All cluster operators have completed progressing 
INFO Checking to see if there is a route at openshift-console/console... 
DEBUG Route found in openshift-console namespace: console 
DEBUG OpenShift console route is admitted          
INFO Install complete!                            
INFO To access the cluster as the system:admin user when using 'oc', run 
INFO     export KUBECONFIG=/home/labadmin/oc41827upi/auth/kubeconfig 
INFO Access the OpenShift web-console here: https://console-openshift-console.apps.oc41827.internal.local 
INFO Login to the console with user: "kubeadmin", and password: "ABcde-fghIKk-lmnpQ-RSTUv" 
DEBUG Time elapsed per stage:                      
DEBUG Cluster Operators Available: 2m23s           
INFO Time elapsed: 2m23s                          
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Security Warning:&lt;/strong&gt; Save that kubeadmin password somewhere secure — you should only use it to create permanent admin users and for very specific tasks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ✅ Verifying Cluster Health
&lt;/h2&gt;

&lt;p&gt;Once the installer reports completion, verify everything is working:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All Nodes Ready&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;oc get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                      STATUS   ROLES                  AGE    VERSION
masteros01.oc41827.internal.local   Ready    control-plane,master   134m   v1.31.14
masteros02.oc41827.internal.local   Ready    control-plane,master   133m   v1.31.14
masteros03.oc41827.internal.local   Ready    control-plane,master   132m   v1.31.14
workeros01.oc41827.internal.local   Ready    worker                 96m    v1.31.14
workeros02.oc41827.internal.local   Ready    worker                 96m    v1.31.14
workeros03.oc41827.internal.local   Ready    worker                 96m    v1.31.14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No Degraded Operators&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;oc get clusteroperator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row should show &lt;code&gt;True&lt;/code&gt; in the Available column, &lt;code&gt;False&lt;/code&gt; in Progressing, and &lt;code&gt;False&lt;/code&gt; in Degraded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                       VERSION   AVAILABLE   PROGRESSING   DEGRADED   SINCE   MESSAGE
authentication                             4.18.37   True        False         False      28m     
baremetal                                  4.18.37   True        False         False      120m 
...
service-ca                                 4.18.37   True        False         False      121m    
storage                                    4.18.37   True        False         False      121m    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cluster seen as available&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;oc get clusterversion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME      VERSION   AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.18.37   True        False         25m     Cluster version is 4.18.37
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧪 Testing Access: The Web Console
&lt;/h2&gt;

&lt;p&gt;The web console is only accessible from within your lab's network unless you configure external routing. On my local workstation, I added entries to &lt;em&gt;/etc/hosts&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.168.110.22 console-openshift-console.apps.oc41827.internal.local
192.168.110.22 oauth-openshift.apps.oc41827.internal.local
192.168.110.21 api.oc41827.internal.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;code&gt;https://console-openshift-console.apps.oc41827.internal.local&lt;/code&gt; and log in with the kubeadmin credentials shown above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvexbib4j5jvscgahsju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvexbib4j5jvscgahsju.png" alt=" " width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🛡️ &lt;strong&gt;Remember:&lt;/strong&gt; The kubeadmin account should be disabled or replaced ASAP. Create proper administrative users instead (we'll cover this in Part 4).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🏃 Stopping and Restarting the Cluster
&lt;/h2&gt;

&lt;p&gt;⚠️ Critical: Always shut down workers before control plane nodes. Shutting down all etcd members simultaneously can cause quorum loss and data corruption.&lt;/p&gt;

&lt;p&gt;Because this is a VM-based lab, I periodically shut down and restarted the cluster. Here's the correct order:&lt;/p&gt;

&lt;h3&gt;
  
  
  Shutdown Order
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Worker nodes&lt;/strong&gt; (in any order):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@workeros03
The authenticity of host &lt;span class="s1"&gt;'workeros03 (192.168.110.113)'&lt;/span&gt; can&lt;span class="s1"&gt;'t be established.
...
---
[core@workeros03 ~]$ sudo shutdown -h now
[core@workeros03 ~]$ Connection to workeros03 closed by remote host.
Connection to workeros03 closed.
$
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@workeros02
...
&lt;span class="nt"&gt;---&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;core@workeros02 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;shutdown &lt;span class="nt"&gt;-h&lt;/span&gt; now
&lt;span class="o"&gt;[&lt;/span&gt;core@workeros02 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;Connection to workeros02 closed by remote host.
Connection to workeros02 closed.
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@workeros01
...
&lt;span class="nt"&gt;---&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;core@workeros01 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;shutdown &lt;span class="nt"&gt;-h&lt;/span&gt; now
&lt;span class="o"&gt;[&lt;/span&gt;core@workeros01 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;Connection to workeros01 closed by remote host.
Connection to workeros01 closed.
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Control plane nodes&lt;/strong&gt; (in any order):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@masteros03
...
&lt;span class="nt"&gt;---&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;core@masteros03 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;shutdown &lt;span class="nt"&gt;-h&lt;/span&gt; now
&lt;span class="o"&gt;[&lt;/span&gt;core@masteros03 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;Connection to masteros03 closed by remote host.
Connection to masteros03 closed.
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@masteros02
...
&lt;span class="nt"&gt;---&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;core@masteros02 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;shutdown &lt;span class="nt"&gt;-h&lt;/span&gt; now
&lt;span class="o"&gt;[&lt;/span&gt;core@masteros02 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;Connection to masteros02 closed by remote host.
Connection to masteros02 closed.
&lt;span class="nv"&gt;$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; .ssh/oc41827upi core@masteros01
...
&lt;span class="nt"&gt;---&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;core@masteros01 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;shutdown &lt;span class="nt"&gt;-h&lt;/span&gt; now
&lt;span class="o"&gt;[&lt;/span&gt;core@masteros01 ~]&lt;span class="nv"&gt;$ &lt;/span&gt;Connection to masteros01 closed by remote host.
Connection to masteros01 closed.
&lt;span class="nv"&gt;$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Startup Order
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Control plane nodes&lt;/strong&gt; (wait for them to stabilize)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker nodes&lt;/strong&gt; (wait for CSR approvals)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After restarting, you might see pending CSRs. Approve them using the batch command from earlier, and nodes will return to &lt;code&gt;Ready&lt;/code&gt; status within 5-10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  📋 Summary: Deployment Checklist
&lt;/h2&gt;

&lt;p&gt;✅ Pull secret downloaded&lt;br&gt;
✅ Ignition files regenerated and served&lt;br&gt;
✅ VMs PXE booted from iPXE menu&lt;br&gt;
✅ Deployment started&lt;br&gt;
✅ Bootstrap monitored via journalctl&lt;br&gt;
✅ Bootstrap removed from HAProxy&lt;br&gt;
✅ CSRs approved&lt;br&gt;
✅ Deployment finished&lt;br&gt;
✅ Cluster operators verified stable  &lt;/p&gt;

&lt;h2&gt;
  
  
  🔜 Coming Up in Part 4
&lt;/h2&gt;

&lt;p&gt;Now that the cluster is running, we need to make it usable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Creating HTPasswd users&lt;/strong&gt; (admin and developer accounts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assigning roles&lt;/strong&gt; (cluster-admin vs project-specific permissions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing logins&lt;/strong&gt; with new credentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disabling the kubeadmin account&lt;/strong&gt; (security best practice)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cluster is alive—but until we configure proper authentication, you're still flying blind with the emergency kubeadmin account. Stay tuned!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Did your deployment run smoothly, or did you hit certificate expiration issues too? Share your experiences in the comments below!&lt;/em&gt; 👇&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>openshift</category>
      <category>redhat</category>
    </item>
    <item>
      <title>Building an OpenShift 4.18 Cluster from Scratch: Part 2 – Generating Ignition Configs &amp; VM Prep</title>
      <dc:creator>Ana Villar</dc:creator>
      <pubDate>Thu, 16 Jul 2026 19:54:28 +0000</pubDate>
      <link>https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-2-generating-ignition-configs-vm-prep-4ka9</link>
      <guid>https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-2-generating-ignition-configs-vm-prep-4ka9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is &lt;strong&gt;Part 2&lt;/strong&gt; of a multi-part series.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;a href="https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-1-the-network-foundation-utilities-server-5g92"&gt;&lt;strong&gt;Part 1: The Network Foundation &amp;amp; Utilities Server&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3: The Deployment Lifecycle &amp;amp; Troubleshooting&lt;/strong&gt; (Coming Soon)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4: Post-Install Hardening &amp;amp; User Management&lt;/strong&gt; (Coming Soon)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Part 1, we built the Utilities Server—DNS, DHCP, TFTP, HTTP, and HAProxy. Now it's time to prepare the actual OpenShift deployment artifacts.&lt;/p&gt;

&lt;p&gt;This part is all about Ignition: what it is, how to generate it, and how to feed it to our VMs so they can boot into Red Hat CoreOS and join the cluster automatically.&lt;/p&gt;

&lt;p&gt;We'll also cover a critical—and poorly documented—step: disabling Secure Boot on libvirt VMs to allow iPXE chainloading. Without this, your nodes will simply refuse to boot.&lt;/p&gt;

&lt;p&gt;Note that all the tasks mentioned in this blog are performed in the utilities server.&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Download OpenShift RHCOS images
&lt;/h2&gt;

&lt;p&gt;To deploy OpenShift, we need to download the Red Hat CoreOS (formerly Red Hat Enterprise Linux CoreOS) images. It's the immutable, container-optimized operating system that OpenShift uses on all cluster nodes—bootstrap, masters, and workers alike.&lt;/p&gt;

&lt;p&gt;RHCOS images (kernel, initramfs, rootfs) are the three core artifacts needed to network-boot RHCOS: the kernel is the Linux kernel that initializes the hardware, the initramfs is a minimal in-memory filesystem that loads early in the boot process and handles loading drivers and fetching the rootfs, and the rootfs is the full RHCOS filesystem that gets loaded into RAM and serves as the base operating system before Ignition takes over.&lt;/p&gt;

&lt;p&gt;Those files can be found in this &lt;a href="https://mirror.openshift.com/pub/openshift-v4/dependencies/rhcos/" rel="noopener noreferrer"&gt;OpenShift mirror site&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;At the time I deployed the cluster, I checked what was the latest available 4.18 version, which happened to be 4.18.27. That is the reason you are going to see some folders called "oc41827". But you can name them as you prefer, but ensure you review the commands and configuration files accordantly.  &lt;/p&gt;

&lt;p&gt;First, let's create the folders to hold those files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/html/oc41827/&lt;span class="o"&gt;{&lt;/span&gt;ignition,rhcos&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define some variables to ease the download process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ ocp_maj&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.18&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;rhcos_ver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.18.27
&lt;span class="nv"&gt;$ mirror&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://mirror.openshift.com/pub/openshift-v4
&lt;span class="nv"&gt;$ baseurl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;mirror&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/dependencies/rhcos/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ocp_maj&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rhcos_ver&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/html/oc1827/rhcos/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And download the files&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;baseurl&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/rhcos-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rhcos_ver&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-x86_64-live-rootfs&lt;/span&gt;.x86_64.img
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;baseurl&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/rhcos-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rhcos_ver&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-x86_64-live-kernel-x86_64&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;baseurl&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/rhcos-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rhcos_ver&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-x86_64-live-initramfs&lt;/span&gt;.x86_64.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that on version 4.20, the kernel file name is &lt;em&gt;rhcos-4.20.0-x86_64-live-kernel.x86_64&lt;/em&gt;, while on 4.18 is &lt;em&gt;rhcos-4.18.27-x86_64-live-kernel-x86_64&lt;/em&gt;. Note the difference between &lt;em&gt;&lt;strong&gt;.x86&lt;/strong&gt;&lt;/em&gt; and &lt;em&gt;&lt;strong&gt;-x86&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Downloading the OpenShift Tools
&lt;/h2&gt;

&lt;p&gt;We need the tools to generate the cluster configuration, which are the OpenShift Installer &amp;amp; OpenShift Client.&lt;/p&gt;

&lt;p&gt;They can be found in the &lt;a href="https://mirror.openshift.com/pub/openshift-v4/clients/ocp/" rel="noopener noreferrer"&gt;OpenShift mirror site&lt;/a&gt;. At the time I was deploying this cluster, the latest 4.18 version available was 4.18.37.&lt;/p&gt;

&lt;p&gt;Let's download them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su
&lt;span class="c"&gt;# mirror="https://mirror.openshift.com/pub/openshift-v4/clients"&lt;/span&gt;
&lt;span class="c"&gt;# wget ${mirror}/ocp/4.18.37/openshift-client-linux-4.18.37.tar.gz&lt;/span&gt;
&lt;span class="c"&gt;# wget ${mirror}/ocp/4.18.37/openshift-install-linux-4.18.37.tar.gz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extract both, move binaries to /usr/bin/ and ensure we have some helping aliases&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# tar -xvf openshift-client-linux-4.18.37.tar.gz -C /usr/bin/&lt;/span&gt;
&lt;span class="c"&gt;# tar -xvf openshift-install-linux-4.18.37.tar.gz -C /usr/bin/&lt;/span&gt;
&lt;span class="c"&gt;# oc completion bash &amp;gt; /etc/bash_completion.d/openshift&lt;/span&gt;
&lt;span class="c"&gt;# openshift-install completion bash &amp;gt; /etc/bash_completion.d/openshift-install&lt;/span&gt;
&lt;span class="c"&gt;# exit&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; /etc/bash_completion.d/openshift
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; /etc/bash_completion.d/openshift-install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now you can test them. You should see something similar to the following lines&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;oc version
&lt;span class="go"&gt;Client Version: 4.18.37
Kustomize Version: v5.4.2
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openshift-install version
&lt;span class="go"&gt;openshift-install 4.18.37
built from commit 488926dc2c95d96460ee9939929a76ed23e1c596
release image quay.io/openshift-release-dev/ocp-release@sha256:9b7068aa6f6087c2f0a7cefa241c5dbb0ede0efaad783607dff0da98cac432d2
release architecture amd64
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Generating installation configuration file
&lt;/h2&gt;

&lt;p&gt;In order to deploy the OpenShift nodes, we need to generate the Ignition files. Those are JSON-based configuration specifications that tell RHCOS exactly how to configure itself on first boot—partitioning disks, writing network configs, embedding the pull secret, and registering the node with the OpenShift cluster. Think of them as the "first-run setup script" that transforms a blank RHCOS machine into a functioning OpenShift node.&lt;/p&gt;

&lt;p&gt;To generate those files, we can runs the OpenShift installer, which asks for the necessary cluster information and then creates the installation configuration file &lt;em&gt;install-config.yaml&lt;/em&gt; accordingly.&lt;/p&gt;

&lt;p&gt;Instead, I have a &lt;em&gt;install-config.yaml&lt;/em&gt; template I used to create the Kubernetes manifests and the ignition configuration files for the bootstrap node &lt;em&gt;bootstrap.ign&lt;/em&gt;, control plane nodes &lt;em&gt;master.ign&lt;/em&gt;, and compute nodes &lt;em&gt;worker.ign&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;install-config.yaml&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Note the importance of the correct contents of this file, as it defines the OpenShift cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 3
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
metadata:
  name: oc41827
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  networkType: OVNKubernetes
  serviceNetwork:
  - 172.30.0.0/16
platform:
  none: {}
fips: false
pullSecret: 'CHANGEME'
sshKey: 'CHANGEMETOO'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What It Defines&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cluster Identity &lt;em&gt;(metadata.name, baseDomain)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Determines all DNS names &lt;em&gt;(api.., *.apps..)&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking &lt;em&gt;(clusterNetwork, serviceNetwork)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Ensures pods, services, and node IPs don't conflict with each other or existing infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Topology &lt;em&gt;(node replicas, control plane size)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Sets the minimum resources needed for high availability (3 masters, at least 1 worker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication &lt;em&gt;(pullSecret, sshKey)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Allows nodes to pull container images from Red Hat registries and enables emergency SSH access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform &lt;em&gt;(platform: none: {})&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Tells the installer we're doing bare metal/external provisioning, not cloud integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As you can see, there are two pieces of information are essential before we can use &lt;em&gt;install-config.yaml&lt;/em&gt; that we need to change.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pull Secret
&lt;/h4&gt;

&lt;p&gt;Log into the &lt;a href="https://console.redhat.com/openshift/install/metal/user-provisioned" rel="noopener noreferrer"&gt;OpenShift console using this link&lt;/a&gt;, or navigate to &lt;em&gt;Red Hat OpenShift&lt;/em&gt;, under &lt;em&gt;Get started with OpenShift&lt;/em&gt; select &lt;em&gt;Create Cluster&lt;/em&gt;, Select &lt;em&gt;Datacenter&lt;/em&gt;, then &lt;em&gt;Bare Metal (x86_64)&lt;/em&gt;, and next select &lt;em&gt;Full Control&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46kjpj9eaz4x9993gfk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46kjpj9eaz4x9993gfk6.png" alt=" " width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Pull secret&lt;/strong&gt;, press &lt;strong&gt;Download the pull secret&lt;/strong&gt;. This file authenticates your cluster with Red Hat's registries.&lt;/p&gt;

&lt;p&gt;Save it and convert it into a one-liner json file. Create a working directory from where the OpenShift deployment will be run, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;oc41827upi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool pull-secret &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pull-secret.json
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;pull-secret.json | jq &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/oc41827upi/pull-secret-online.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  SSH key
&lt;/h4&gt;

&lt;p&gt;You'll need an SSH key pair to access the nodes from the utilities server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .ssh/oc41827upi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make note of the public key path—we'll reference it in the install config.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update &lt;em&gt;install-config.yaml&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Now it is time to get all the information together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt; ~/oc41827upi/install-config.yaml
apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 3
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
metadata:
  name: oc41827
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  networkType: OVNKubernetes
  serviceNetwork:
  - 172.30.0.0/16
platform:
  none: {}
fips: false
pullSecret: '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/oc41827upi/pull-secret.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'
sshKey: '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/oc41827upi.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;'
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Important: The metadata.name (oc41827) becomes part of your cluster's FQDN. Make sure this matches the DNS records we created in Part 1 (api.oc41827.internal.local).&lt;/p&gt;

&lt;p&gt;🔒 Sensitive Data: The pullSecret and sshKey fields are populated at generation time. Once you run openshift-install create manifests, these values are embedded in the Ignition files. Do not commit install-config.yaml to a public repository without redacting these fields.&lt;/p&gt;

&lt;p&gt;Key Fields Explained&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseDomain&lt;/td&gt;
&lt;td&gt;internal.local&lt;/td&gt;
&lt;td&gt;Matches the DNS zone from Part 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata.name&lt;/td&gt;
&lt;td&gt;oc41827&lt;/td&gt;
&lt;td&gt;Cluster identifier, part of the FQDN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;platform&lt;/td&gt;
&lt;td&gt;none: {}&lt;/td&gt;
&lt;td&gt;Platform-agnostic (bare metal / VM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;networkType&lt;/td&gt;
&lt;td&gt;OVNKubernetes&lt;/td&gt;
&lt;td&gt;Default SDN for 4.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replicas (master)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Minimum for HA control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;replicas (worker)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Adjust as needed for your lab, minimum 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Generating Manifests &amp;amp; Ignition Files
&lt;/h2&gt;

&lt;p&gt;Now we transform the blueprint into machine-readable configuration. This is a one-way operation: once you generate manifests, you can make targeted edits, but after Ignition files are created, the config is frozen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Manifests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/oc41827upi
&lt;span class="nv"&gt;$ &lt;/span&gt;openshift-install create manifests &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates two directories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manifests/ — YAML definitions for cluster resources&lt;/li&gt;
&lt;li&gt;openshift/ — Internal OpenShift configuration files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Optional: At this stage, you can modify files in manifests/ to customize the cluster before Ignition generation. Common modifications include adjusting default node settings or removing the infrastructure components you don't need. For this lab, we'll proceed with defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Generate Ignition Files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;openshift-install create ignition-configs &lt;span class="nt"&gt;--dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces three critical files:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bootstrap.ign&lt;/td&gt;
&lt;td&gt;The temporary bootstrap node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;master.ign&lt;/td&gt;
&lt;td&gt;Each control plane node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker.ign&lt;/td&gt;
&lt;td&gt;Each compute node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These Ignition files contain the complete initial configuration for each node type: disk partitioning, systemd units, network settings, and the embedded pull secret. They are the single source of truth that tells RHCOS how to become an OpenShift node.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Serving Ignition Files via Apache
&lt;/h2&gt;

&lt;p&gt;Copy the Ignition files to the web server&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; ~/oc41827upi/bootstrap.ign /var/www/html/oc41827/ignition/
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; ~/oc41827upi/master.ign /var/www/html/oc41827/ignition/
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; ~/oc41827upi/worker.ign /var/www/html/oc41827/ignition/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set permissions&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +r /var/www/html/oc41827/ignition/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify they are accessible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://192.168.110.20/oc41827/ignition/bootstrap.ign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Expected: HTTP/1.1 200 OK&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Creating the boot.ipxe Menu
&lt;/h2&gt;

&lt;p&gt;Now we create the iPXE boot script to include OpenShift entries. Each node type points to the same kernel and rootfs but fetches a different Ignition file via the coreos.inst.ignition_url kernel argument.&lt;/p&gt;

&lt;p&gt;Here's the structure for the menu entries in your boot.ipxe (&lt;em&gt;/var/www/html/boot.ipxe&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!ipxe
dhcp

set base http://192.168.110.20/
set oc41827 http://192.168.110.20/oc41827/

:bootstrap
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/bootstrap.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:master
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/master.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:worker
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/worker.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Disk Device Name: coreos.inst.install_dev=vda assumes your VMs use virtio disks. If your VMs use SATA or SCSI, change this to sda accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Testing the environment
&lt;/h2&gt;

&lt;p&gt;Before we continue, I would recommend to create a test virtual machine and to check if it can be installed using iPXE. This step can be bypassed, but it can give us the confidence that all is ready for the OpenShift deployment.&lt;/p&gt;

&lt;p&gt;I am adding here two examples, RHEL and Ubuntu&lt;/p&gt;

&lt;h3&gt;
  
  
  RHEL 10
&lt;/h3&gt;

&lt;p&gt;Previously, I copied the RHEL 10 iso image in the utilities server in a folder called /isos. Let's mount it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /isos
rhel-10.1-x86_64-dvd.iso
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; /mnt/dvd
&lt;span class="nb"&gt;mkdir&lt;/span&gt;: created directory &lt;span class="s1"&gt;'/mnt/dvd'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-v&lt;/span&gt; /isos/rhel-10.1-x86_64-dvd.iso /mnt/dvd
mount: /mnt/dvd: WARNING: &lt;span class="nb"&gt;source &lt;/span&gt;write-protected, mounted read-only.
mount: /dev/loop0 mounted on /mnt/dvd.
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let´s copy now the contents of the iso image to our Apache server folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; /var/www/html/rhel10
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /mnt/dvd/&lt;span class="k"&gt;*&lt;/span&gt; /var/www/html/rhel10/
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;restorecon &lt;span class="nt"&gt;-Rv&lt;/span&gt; /var/www/html
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ubuntu 24.04 and 26.04
&lt;/h3&gt;

&lt;p&gt;Similarly as with RHEL, I copied the isos to the server in the /isos folder.&lt;/p&gt;

&lt;p&gt;Place their contents into the web server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/html/ubuntuserver/24.04
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/www/html/ubuntuserver/26.04
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop /isos/ubuntu-24.04.4-live-server-amd64.iso /mnt/dvd
mount: /mnt/dvd: WARNING: &lt;span class="nb"&gt;source &lt;/span&gt;write-protected, mounted read-only.
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/dvd/. /var/www/html/ubuntuserver/24.04/
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /mnt/dvd
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop /isos/ubuntu-26.04-live-server-amd64.iso /mnt/dvd
mount: /mnt/dvd: WARNING: &lt;span class="nb"&gt;source &lt;/span&gt;write-protected, mounted read-only.
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /mnt/dvd/. /var/www/html/ubuntuserver/26.04/
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /mnt/dvd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct SELinux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;restorecon &lt;span class="nt"&gt;-Rv&lt;/span&gt; /var/www/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update the iPXE boot file
&lt;/h3&gt;

&lt;p&gt;This is the full contents of the ipxe boot file.&lt;/p&gt;

&lt;p&gt;/var/www/html/boot.ipxe&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!ipxe
dhcp

set base http://192.168.110.20/
set rhel10 http://192.168.110.20/rhel10/
set ubuntuserver2404 http://192.168.110.20/ubuntuserver/24.04/
set ubuntuserver2604 http://192.168.110.20/ubuntuserver/26.04/
set oc41827 http://192.168.110.20/oc41827/

menu iPXE menu
item redhat    Install RHEL 10 interactively
item ubuntuserver2404  Install Ubuntu Server 24.04.4 LTS interactively
item ubuntuserver2604  Install Ubuntu Server 26.04 LTS interactively
item bootstrap  Install OpenShift 4.18.27 Bootstrap
item master  Install OpenShift 4.18.27 Master Node
item worker  Install OpenShift 4.18.27 Worker Node
item shell      Drop to iPXE shell
choose --default shell --timeout 1500000 target &amp;amp;&amp;amp; goto ${target}

:redhat
kernel ${rhel10}images/pxeboot/vmlinuz ip=dhcp inst.repo=${rhel10} inst.stage2=${rhel10} quiet
initrd ${rhel10}images/pxeboot/initrd.img
boot

:ubuntuserver2404
kernel ${ubuntuserver2404}casper/vmlinuz ip=dhcp url=${ubuntuserver2404}
initrd ${ubuntuserver2404}casper/initrd
boot

:ubuntuserver2604
kernel ${ubuntuserver2604}casper/vmlinuz ip=dhcp url=${ubuntuserver2604}
initrd ${ubuntuserver2604}casper/initrd
boot

:bootstrap
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/bootstrap.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:master
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/master.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:worker
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/worker.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:shell
shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Disabling Secure Boot on VMs
&lt;/h2&gt;

&lt;p&gt;This is the step that cost me several hours of debugging. If your libvirt VMs have Secure Boot enabled, iPXE will fail to chainload. The UEFI firmware rejects the unsigned iPXE binary, and your VMs won't boot.&lt;/p&gt;

&lt;p&gt;Those steps are to be done in your hypervisor, where all the virtual machines are being hosted (the utilities server, your test virtual machine to boot from ipxe, the OpenShift nodes).&lt;/p&gt;

&lt;p&gt;If you are using a different hypervisor, check their documentation to disable Secure Boot. Here I am explaining how I managed to do it for RHEL virtualization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dump the VM definition as XML
&lt;/h3&gt;

&lt;p&gt;Replace &lt;em&gt;&lt;strong&gt;test&lt;/strong&gt;&lt;/em&gt; by the name of your virtual machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh dumpxml &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Edit the VM definition
&lt;/h3&gt;

&lt;p&gt;Using your preferred editor (for example vim or nano), edit the file &lt;em&gt;test.xml&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Find the  section and change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;vcpu placement='static'&amp;gt;2&amp;lt;/vcpu&amp;gt;
  &amp;lt;os firmware='efi'&amp;gt;
    &amp;lt;type arch='x86_64' machine='pc-q35-rhel10.0.0'&amp;gt;hvm&amp;lt;/type&amp;gt;
    &amp;lt;firmware&amp;gt;
      &amp;lt;feature enabled='yes' name='enrolled-keys'/&amp;gt;
      &amp;lt;feature enabled='yes' name='secure-boot'/&amp;gt;
    &amp;lt;/firmware&amp;gt;
    &amp;lt;loader readonly='yes' secure='yes' type='pflash' format='raw'&amp;gt;/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd&amp;lt;/loader&amp;gt;
    &amp;lt;nvram template='/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd' templateFormat='raw' format='raw'&amp;gt;/var/lib/libvirt/qemu/nvram/test_VARS.fd&amp;lt;/nvram&amp;gt;
  &amp;lt;/os&amp;gt;
  &amp;lt;features&amp;gt;
    &amp;lt;acpi/&amp;gt;
    &amp;lt;apic/&amp;gt;
    &amp;lt;smm state='on'/&amp;gt;
  &amp;lt;/features&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;vcpu placement='static'&amp;gt;2&amp;lt;/vcpu&amp;gt;
  &amp;lt;os&amp;gt;
    &amp;lt;type arch='x86_64' machine='pc-q35-rhel10.0.0'&amp;gt;hvm&amp;lt;/type&amp;gt;
    &amp;lt;loader readonly='yes' type='pflash' format='raw'&amp;gt;/usr/share/edk2/ovmf/OVMF_CODE.fd&amp;lt;/loader&amp;gt;
    &amp;lt;nvram template='/usr/share/edk2/ovmf/OVMF_VARS.fd' templateFormat='raw' format='raw'&amp;gt;/var/lib/libvirt/qemu/nvram/test_VARS.fd&amp;lt;/nvram&amp;gt;
  &amp;lt;/os&amp;gt;
  &amp;lt;features&amp;gt;
    &amp;lt;acpi/&amp;gt;
    &amp;lt;apic/&amp;gt;
    &amp;lt;smm state='off'/&amp;gt;
  &amp;lt;/features&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔑 Key Changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;&amp;lt;os firmware='efi'&amp;gt;&lt;/code&gt; by &lt;code&gt;&amp;lt;os&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove the full section starting with &lt;code&gt;&amp;lt;firmware&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;/firmware&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;At the line starting with &lt;code&gt;&amp;lt;loader&lt;/code&gt;, remove &lt;em&gt;secure='yes'&lt;/em&gt; and replace &lt;em&gt;OVMF_CODE.secboot.fd&lt;/em&gt; by &lt;em&gt;OVMF_CODE.fd&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;At the line starting with &lt;code&gt;&amp;lt;nvram&lt;/code&gt;, replace &lt;em&gt;OVMF_VARS.secboot.fd&lt;/em&gt; by &lt;em&gt;OVMF_VARS.fd&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;And replace the line &lt;code&gt;&amp;lt;smm state='on'/&amp;gt;&lt;/code&gt; by &lt;code&gt;&amp;lt;smm state='off'/&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Update the VM definition
&lt;/h3&gt;

&lt;p&gt;Update the configuration of the virtual machine in your host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh define test.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Verification
&lt;/h2&gt;

&lt;p&gt;After disabling Secure Boot on the VM, power on the VM and connect to its console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;virsh start &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--console&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or using your Cockpit interface.&lt;/p&gt;

&lt;p&gt;You should now see the iPXE menu appear. If it loads the boot.ipxe script correctly, and you can install the OS into your test VM, you're ready for deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2 Recap
&lt;/h2&gt;

&lt;p&gt;Let's review what we accomplished:&lt;/p&gt;

&lt;p&gt;✅ Downloaded OpenShift installer and CLI tools&lt;br&gt;
✅ Secured the pull secret and SSH key&lt;br&gt;
✅ Wrote install-config.yaml matching our DNS/DHCP from Part 1&lt;br&gt;
✅ Generated manifests and Ignition files&lt;br&gt;
✅ Published Ignition files and RHCOS images via Apache&lt;br&gt;
✅ Updated boot.ipxe with OpenShift boot entries&lt;br&gt;
✅ Disabled Secure Boot on all VMs to enable iPXE chainloading&lt;/p&gt;

&lt;p&gt;Everything is staged. The Ignition files are on the web server, the iPXE menu is updated, and the VMs can boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔜 Coming Up in Part 3: The Deployment Lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Powering on the bootstrap node and monitoring logs&lt;/li&gt;
&lt;li&gt;Waiting for bootstrap-complete and removing the bootstrap node&lt;/li&gt;
&lt;li&gt;Approving CSRs for master and worker nodes&lt;/li&gt;
&lt;li&gt;Verifying cluster health with oc commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>openshift</category>
      <category>redhat</category>
      <category>ipxe</category>
    </item>
    <item>
      <title>Building an OpenShift 4.18 Cluster from Scratch: Part 1 – The Network Foundation &amp; Utilities Server</title>
      <dc:creator>Ana Villar</dc:creator>
      <pubDate>Tue, 30 Jun 2026 19:21:50 +0000</pubDate>
      <link>https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-1-the-network-foundation-utilities-server-5g92</link>
      <guid>https://dev.to/vilan011/building-an-openshift-418-cluster-from-scratch-part-1-the-network-foundation-utilities-server-5g92</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is &lt;strong&gt;Part 1&lt;/strong&gt; of a multi-part series.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[Part 2: Generating Ignition Configs &amp;amp; VM Prep]&lt;/strong&gt; &lt;em&gt;(Coming Soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Part 3: The Deployment Lifecycle &amp;amp; Troubleshooting]&lt;/strong&gt; &lt;em&gt;(Coming Soon)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Part 4: Post-Install Hardening &amp;amp; User Management]&lt;/strong&gt; &lt;em&gt;(Coming Soon)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this series, I walk through a full &lt;strong&gt;OpenShift Container Platform 4.18 deployment from scratch&lt;/strong&gt;. While most tutorials target cloud providers or pre-provisioned infrastructure, this guide replicates a true &lt;strong&gt;platform-agnostic (bare metal)&lt;/strong&gt; installation—executed within a lab of Virtual Machines hosted on &lt;strong&gt;RHEL 10&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This approach allows us to master the critical underlying infrastructure—DNS, DHCP, PXE booting, and load balancing—without the cost of physical hardware. By the end of Part 1, you will have a fully functional &lt;strong&gt;Utilities Server&lt;/strong&gt; that acts as the command center for the entire cluster, handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS&lt;/strong&gt;: Internal resolution for all cluster nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DHCP&lt;/strong&gt;: IP allocation and iPXE boot redirection via Kea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TFTP &amp;amp; HTTP&lt;/strong&gt;: Serving bootloader images, RHCOS ISOs, and Ignition configs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HAProxy&lt;/strong&gt;: Load balancing for the Kubernetes API and Ingress traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We won't run &lt;em&gt;openshift-install&lt;/em&gt; just yet; first, we must build the foundation upon which the cluster relies.&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 References &amp;amp; Background
&lt;/h2&gt;

&lt;p&gt;This lab strictly follows the &lt;strong&gt;Platform-Agnostic&lt;/strong&gt; installation methodology defined by Red Hat. The setup was heavily inspired by the &lt;strong&gt;DO322: Red Hat OpenShift Installation Lab&lt;/strong&gt; training course, ensuring we cover the same critical edge cases and network requirements found in production environments.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.18/html/installing_on_any_platform/installing-platform-agnostic" rel="noopener noreferrer"&gt;OpenShift Container Platform 4.18 Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.redhat.com/en/services/training/do322-red-hat-openshift-installation-lab" rel="noopener noreferrer"&gt;DO322 Course Page&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why use VMs for a "Bare Metal" guide&lt;/strong&gt;? While running on virtual machines provides speed and portability for this lab, the network stack (DNS/DHCP/PXE) and the Ignition configuration flow are &lt;strong&gt;identical&lt;/strong&gt; to what you would encounter on physical hardware. Following the DO322 methodology ensures this tutorial remains relevant for real-world bare-metal deployments.&lt;/p&gt;

&lt;p&gt;Let's get the foundation laid.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Lab Environment Overview
&lt;/h2&gt;

&lt;p&gt;Before diving in, here is the topology I'm working with. All these VMs are hosted on a RHEL 10 hypervisor.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;VM Name&lt;/th&gt;
&lt;th&gt;IP Address&lt;/th&gt;
&lt;th&gt;Specs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Utilities Server&lt;/td&gt;
&lt;td&gt;utilities&lt;/td&gt;
&lt;td&gt;192.168.110.20&lt;/td&gt;
&lt;td&gt;4 vCPU, 8GB RAM, 250GB Disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bootstrap Node&lt;/td&gt;
&lt;td&gt;bootstos&lt;/td&gt;
&lt;td&gt;192.168.110.100&lt;/td&gt;
&lt;td&gt;4 vCPU, 16GB RAM, 100GB NVMe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control Plane&lt;/td&gt;
&lt;td&gt;masteros01-03&lt;/td&gt;
&lt;td&gt;.101 to .103&lt;/td&gt;
&lt;td&gt;4 vCPU, 24GB RAM, 100GB NVMe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute Nodes&lt;/td&gt;
&lt;td&gt;workeros01-03&lt;/td&gt;
&lt;td&gt;.111 to .113&lt;/td&gt;
&lt;td&gt;2 vCPU, 8GB RAM, 100GB SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Setting Up the Utilities Server
&lt;/h2&gt;

&lt;p&gt;We start with a fresh RHEL 10 installation. Once installed, we register the system and ensure all packages are up to date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;subscription-manager register &lt;span class="nt"&gt;--username&lt;/span&gt; &amp;lt;YOUR_USER&amp;gt; &lt;span class="nt"&gt;--password&lt;/span&gt; &amp;lt;PASSWORD&amp;gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf update redhat-release
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After reboot, we verify connectivity and prepare for the services.&lt;/p&gt;

&lt;h4&gt;
  
  
  Network Configuration
&lt;/h4&gt;

&lt;p&gt;The utilities server has two interfaces. I configured them via &lt;em&gt;nmtui&lt;/em&gt; to assign static IPs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enp1s0: 192.168.1.20 (Management/Internet Gateway)&lt;/li&gt;
&lt;li&gt;enp7s0: 192.168.110.20 (Internal Cluster Network)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and then, add two extra IP addresses to interface enp7s0, 192.168.110.21 and 192.168.110.22, to be used by OpenShift services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nmcli
&lt;span class="go"&gt;enp1s0: connected to enp1s0
        "Red Hat Virtio 1.0"
        ethernet (virtio_net), 52:54:00:C5:C1:9E, hw, mtu 1500
        ip4 default
        inet4 192.168.1.20/24
        route4 default via 192.168.1.1 metric 100
        route4 192.168.1.0/24 metric 100
        inet6 fe80::5054:ff:fec5:c19e/64
        route6 fe80::/64 metric 1024

enp7s0: connected to enp7s0
        "Red Hat Virtio 1.0"
        ethernet (virtio_net), 52:54:00:E6:D1:A4, hw, mtu 1500
        inet4 192.168.110.22/24
        inet4 192.168.110.21/24
        inet4 192.168.110.20/24
        route4 192.168.110.0/24 metric 101
        route4 192.168.110.0/24 metric 101
        route4 192.168.110.0/24 metric 101
        inet6 fe80::5054:ff:fee6:d1a4/64
        route6 fe80::/64 metric 1024
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Installing &amp;amp; Configuring DNS (BIND)
&lt;/h2&gt;

&lt;p&gt;OpenShift relies heavily on proper DNS. We need records for the API VIPs, ingress VIP, and every node.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install bind &lt;/span&gt;bind-utils &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;I modified the default config (/etc/named.conf) to listen on both interfaces and allow recursion only for our internal subnets. Crucially, we define our zones here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/named.conf snippet
options {
    listen-on port 53 { 127.0.0.1; 192.168.1.20; 192.168.110.20; };
    allow-query { localhost; 192.168.1.0/24; 192.168.110.0/24; };
    allow-recursion { localhost; 192.168.1.0/24; 192.168.110.0/24; };
    forwarders { 8.8.8.8; 8.8.4.4; }; # Or your ISP DNS
    recursion yes;
};

zone "internal.local" {
    type master;
    file "internal.local.zone";
    allow-query { any; };
};

zone "110.168.192.in-addr.arpa" {
    type master;
    file "110.168.192.in-addr.arpa.zone";
    allow-query { any; };
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Zone Files
&lt;/h3&gt;

&lt;p&gt;We create the forward and reverse lookup zones. Notice the VIPs for the API (192.168.110.21) and Ingress (192.168.110.22). These point to our HAProxy server (which runs on the utilities node).&lt;/p&gt;

&lt;p&gt;/var/named/internal.local.zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$TTL 8h
@ IN SOA ns1.internal.local. hostmaster.internal.local. (
    2026042601 ; serial
    1d         ; refresh
    3h         ; retry
    3d         ; expire
    3h )       ; minimum

IN NS ns1.internal.local.

ntp               IN A    192.168.110.20
dns               IN A    192.168.110.20

; Static infrastructure
;hostname IN A 192.168.110.x
utilities.internal.local.  IN A    192.168.110.20

; OpenShift VIPs
api.oc41827.internal.local.       IN A    192.168.110.21
api-int.oc41827.internal.local.   IN A    192.168.110.21
*.apps.oc41827.internal.local.    IN A    192.168.110.22

; Bootstrap Node
bootstos.oc41827.internal.local.     IN A 192.168.110.100

; Master Nodes
masteros01.oc41827.internal.local. IN A 192.168.110.101
masteros02.oc41827.internal.local. IN A 192.168.110.102
masteros03.oc41827.internal.local. IN A 192.168.110.103

; Worker Nodes
workeros01.oc41827.internal.local. IN A 192.168.110.111
workeros02.oc41827.internal.local. IN A 192.168.110.112
workeros03.oc41827.internal.local. IN A 192.168.110.113
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/var/named/110.168.192.in-addr.arpa.zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$TTL 8h
@ IN SOA ns1.internal.local. hostmaster.internal.local. (
          2026041901 ; serial number
          1d         ; refresh period
          3h         ; retry period
          3d         ; expire time
          3h )       ; minimum TTL

 IN NS   ns1.internal.local.

20                IN PTR  ns1.internal.local.
20                IN PTR  utilities.internal.local.

21    IN PTR api.oc41827.internal.local.
21    IN PTR api-int.oc41827.internal.local.

100  IN PTR bootstos.oc41827.internal.local.
101  IN PTR masteros01.oc41827.internal.local.
102  IN PTR masteros02.oc41827.internal.local.
103  IN PTR masteros03.oc41827.internal.local.

111  IN PTR workeros01.oc41827.internal.local.
112  IN PTR workeros02.oc41827.internal.local.
113  IN PTR workeros03.oc41827.internal.local.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Edit&lt;/strong&gt;: &lt;em&gt;oc41827&lt;/em&gt; is the metadata name I will provide to the OpenShift cluster, and it becomes part of the FQDN of those nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check configuration and start the service
&lt;/h3&gt;

&lt;p&gt;Change ownership and permissions, and check the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;named-checkconf
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:named /var/named/internal.local.zone
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:named /var/named/110.168.192.in-addr.arpa.zone
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /var/named/internal.local.zone
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /var/named/110.168.192.in-addr.arpa.zone
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;named-checkzone internal.local /var/named/internal.local.zone
zone internal.local/IN: loaded serial 2026041901
OK
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;named-checkzone 110.168.192.in-addr.arpa /var/named/110.168.192.in-addr.arpa.zone
zone 110.168.192.in-addr.arpa/IN: loaded serial 2026041901
OK
&lt;span class="nv"&gt;$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start the service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; named
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and allow the service through the firewall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dns
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Time Synchronization (Chrony)
&lt;/h2&gt;

&lt;p&gt;Kubernetes clusters are extremely sensitive to time skew. If the clocks drift, certificates and authentication will fail immediately.&lt;/p&gt;

&lt;p&gt;We install chrony (it's already present in RHEL, but we configure it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/chrony.conf snippet
pool 2.rhel.pool.ntp.org iburst
allow 192.168.1.0/24
allow 192.168.110.0/24
local stratum 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the utility server to act as a local NTP source for all nodes while syncing itself with external sources.&lt;/p&gt;

&lt;p&gt;Enable and start the service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; chronyd
Created symlink &lt;span class="s1"&gt;'/etc/systemd/system/multi-user.target.wants/chronyd.service'&lt;/span&gt; → &lt;span class="s1"&gt;'/usr/lib/systemd/system/chronyd.service'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and allow the service through the firewall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ntp &lt;span class="nt"&gt;--permanent&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. DHCP with Kea
&lt;/h2&gt;

&lt;p&gt;Instead of the legacy ISC DHCP server, I used Kea, which is the recommended choice for RHEL 10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation &amp;amp; Config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;kea &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration /etc/kea/kea-dhcp4.conf is where the magic happens for PXE booting. We define client classes to distinguish between iPXE clients and standard UEFI/BIOS PXE clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Dhcp4": {
    "interfaces-config": {
      "interfaces": [ "enp7s0/192.168.110.20" ]
    },

    "client-classes": [
        { 
            "name": "iPXE Clients", 
            "test": "option[175].exists", 
            "option-data": [
               { "name": "tftp-server-name", "data": "192.168.110.20" },
               { "name": "boot-file-name", "data": "http://192.168.110.20/boot.ipxe" }
            ]
        },
        {
            "name": "UEFI PXE Clients",
            "test": "option[93].hex == 0x0007 and not option[175].exists",
            "next-server": "192.168.110.20",
            "boot-file-name": "ipxe-snponly-x86_64.efi"
        },
        {
            "name": "BIOS PXE Clients",
            "test": "option[93].hex == 0x0000 and not option[175].exists",
            "next-server": "192.168.110.20",
            "boot-file-name": "undionly.kpxe"
        }
    ],

    "subnet4": [
      {
        "id": 1,
        "subnet": "192.168.110.0/24",
        "pools": [
          {
            "pool": "192.168.110.200 - 192.168.110.250"
          }
        ],

        "reservations": [
          { "hw-address": "AA:BB:CC:DD:EE:01", "ip-address": "192.168.110.100", "hostname": "bootstos.oc41827.internal.local" },

          { "hw-address": "AA:BB:CC:DD:EE:02", "ip-address": "192.168.110.101", "hostname": "masteros01.oc41827.internal.local" },
          { "hw-address": "AA:BB:CC:DD:EE:03", "ip-address": "192.168.110.102", "hostname": "masteros02.oc41827.internal.local" },
          { "hw-address": "AA:BB:CC:DD:EE:04", "ip-address": "192.168.110.103", "hostname": "masteros03.oc41827.internal.local" },

          { "hw-address": "AA:BB:CC:DD:EE:05", "ip-address": "192.168.110.111", "hostname": "workeros01.oc41827.internal.local" },
          { "hw-address": "AA:BB:CC:DD:EE:06", "ip-address": "192.168.110.112", "hostname": "workeros02.oc41827.internal.local" },
          { "hw-address": "AA:BB:CC:DD:EE:07", "ip-address": "192.168.110.113", "hostname": "workeros03.oc41827.internal.local" }
        ],

        "option-data": [
          { "name": "routers", "code": 3, "data": "192.168.110.1" },
          { "name": "domain-name-servers", "code": 6, "space": "dhcp4", "data": "192.168.110.20" },
          { "name": "domain-name", "data": "internal.local" },
          { "name": "domain-search", "data": "oc41827.internal.local internal.local" }
        ]

      },
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Ensure you replace the MAC addresses with the actual ones assigned to your VMs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the configuration and enable Kea
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kea-dhcp4 &lt;span class="nt"&gt;-t&lt;/span&gt; /etc/kea/kea-dhcp4.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might see some warnings, you might ignore them (but do not ignore any error), see this as an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kea-dhcp4 &lt;span class="nt"&gt;-t&lt;/span&gt; /etc/kea/kea-dhcp4.conf
&lt;span class="go"&gt;2026-04-21 20:52:38.159 WARN  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_MT_DISABLED_QUEUE_CONTROL disabling dhcp queue control when multi-threading is enabled.
2026-04-21 20:52:38.159 WARN  [kea-dhcp4.dhcp4/6313.140336065439872] DHCP4_RESERVATIONS_LOOKUP_FIRST_ENABLED Multi-threading is enabled and host reservations lookup is always performed first.
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_CFGMGR_NEW_SUBNET4 a new subnet has been added to configuration: 192.168.110.0/24 with params: valid-lifetime=7200
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_CFGMGR_SOCKET_TYPE_SELECT using socket type raw
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_CFGMGR_USE_ADDRESS listening on address 192.168.110.20, on interface enp7s0
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_CFGMGR_SOCKET_TYPE_DEFAULT "dhcp-socket-type" not specified , using default socket type raw
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_LEASE_MGR_BACKENDS_REGISTERED the following lease backend types are available: memfile
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.hosts/6313.140336065439872] HOSTS_BACKENDS_REGISTERED the following host backend types are available: 
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.dhcpsrv/6313.140336065439872] DHCPSRV_FORENSIC_BACKENDS_REGISTERED the following forensic backend types are available: 
2026-04-21 20:52:38.160 INFO  [kea-dhcp4.database/6313.140336065439872] CONFIG_BACKENDS_REGISTERED the following config backend types are available: 
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; kea-dhcp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and allow the service through the firewall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dhcp &lt;span class="nt"&gt;--permanent&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Web Services &amp;amp; iPXE Boot
&lt;/h2&gt;

&lt;p&gt;OpenShift uses a live-boot mechanism. We need an HTTP server to serve the RHCOS kernel, rootfs, and ignition files, plus TFTP for the initial bootloader.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apache Setup
&lt;/h3&gt;

&lt;p&gt;Install Apache&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;httpd &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To prevent interferences with haproxy, change the listen port to include the IP address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/httpd/conf/httpd.conf 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Listen 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Listen 192.168.110.20:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allow the service through the firewall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http &lt;span class="nt"&gt;--permanent&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https &lt;span class="nt"&gt;--permanent&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and enable and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; httpd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TFTP Setup
&lt;/h3&gt;

&lt;p&gt;Install TFTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;tftp-server &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; tftp.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and allow the service through the firewall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tftp &lt;span class="nt"&gt;--permanent&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iPXE Setup
&lt;/h3&gt;

&lt;p&gt;We are going to deploy the OpenShift nodes using ipxe and tftp-server. The key is the boot.ipxe script, which we will discuss in the next blog. For now, we prepare iPXE.&lt;/p&gt;

&lt;p&gt;Install iPXE&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;ipxe-bootimgs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and prepare the basic structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /usr/share/ipxe/undionly.kpxe /var/lib/tftpboot/
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /usr/share/ipxe/ipxe-snponly-x86_64.efi /var/lib/tftpboot/
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;restorecon &lt;span class="nt"&gt;-Rv&lt;/span&gt; /var/lib/tftpboot

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can be later used to install other OS, like RHEL or Ubuntu.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Load Balancing with HAProxy
&lt;/h2&gt;

&lt;p&gt;This is critical for High Availability. HAProxy listens on the API VIP (192.168.110.21) and Ingress VIP (192.168.110.22) and forwards traffic to the backend nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install HAProxy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;haproxy &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  HAProxy Configuration for Openshift
&lt;/h3&gt;

&lt;p&gt;Use nano or vim to update the configuration file  (/etc/haproxy/haproxy.cfg). See below the changes I made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/haproxy/haproxy.cfg snippet
    chroot      /var/lib/haproxy
    #pidfile     /var/run/haproxy.pid

defaults
    mode                    tcp
    log                     global
    option                  tcplog
    retries                 3
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    maxconn                 3000

#---------------------------------------------------------------------
# round robin balancing for RHOCP Kubernetes API Server
#---------------------------------------------------------------------
 listen api-server-6443
   bind 192.168.110.21:6443
   mode tcp
   option httpchk GET /readyz HTTP/1.0
   option log-health-checks
   balance roundrobin
   server bootstos bootstos.oc41827.internal.local:6443 verify none check check-ssl inter 10s fall 2 rise 3 backup
   server masteros01 masteros01.oc41827.internal.local:6443 weight 1 verify none check check-ssl inter 10s fall 2 rise 3
   server masteros02 masteros02.oc41827.internal.local:6443 weight 1 verify none check check-ssl inter 10s fall 2 rise 3
   server masteros03 masteros03.oc41827.internal.local:6443 weight 1 verify none check check-ssl inter 10s fall 2 rise 3

# ---------------------------------------------------------------------
# round robin balancing for RHOCP Machine Config Server
# ---------------------------------------------------------------------
 listen machine-config-server-22623
   bind 192.168.110.21:22623
   mode tcp
   server bootstos bootstos.oc41827.internal.local:22623 check inter 1s backup
   server masteros01 masteros01.oc41827.internal.local:22623 check inter 1s
   server masteros02 masteros02.oc41827.internal.local:22623 check inter 1s
   server masteros03 masteros03.oc41827.internal.local:22623 check inter 1s

# ---------------------------------------------------------------------
# round robin balancing for RHOCP Ingress Insecure Port
# ---------------------------------------------------------------------
 listen ingress-router-80
   bind 192.168.110.22:80
   mode tcp
   balance source
   server workeros01 workeros01.oc41827.internal.local:80 check inter 1s
   server workeros02 workeros02.oc41827.internal.local:80 check inter 1s
   server workeros03 workeros03.oc41827.internal.local:80 check inter 1s

# ---------------------------------------------------------------------
# round robin balancing for RHOCP Ingress Secure Port
# ---------------------------------------------------------------------
 listen ingress-router-443
   bind 192.168.110.22:443
   mode tcp
   balance source
   server workeros01 workeros01.oc41827.internal.local:443 check inter 1s
   server workeros02 workeros02.oc41827.internal.local:443 check inter 1s
   server workeros03 workeros03.oc41827.internal.local:443 check inter 1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating firewalld and SELinux
&lt;/h3&gt;

&lt;p&gt;We need to allow the tcp ports used by OpenShift through firewalld&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6443/tcp
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;22623/tcp
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and also ensure SELinux allows those ports to be used as HTTP ports&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;semanage port &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; http_port_t &lt;span class="nt"&gt;-p&lt;/span&gt; tcp 6443
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;semanage port &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; http_port_t &lt;span class="nt"&gt;-p&lt;/span&gt; tcp 6443
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;semanage port &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; http_port_t &lt;span class="nt"&gt;-p&lt;/span&gt; tcp 22623
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;semanage port &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; http_port_t &lt;span class="nt"&gt;-p&lt;/span&gt; tcp 22623
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration check and enabling the HAProxy service
&lt;/h3&gt;

&lt;p&gt;Check the configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;haproxy &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/haproxy/haproxy.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that no output from the previous command is good news!&lt;/p&gt;

&lt;p&gt;and enable and start the service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; haproxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔜 Coming Up in Part 2
&lt;/h2&gt;

&lt;p&gt;Now that the network is breathing correctly, we need to generate the Ignition files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloading RHCOS and OpenShift CLI tools.&lt;/li&gt;
&lt;li&gt;Creating the install-config.yaml.&lt;/li&gt;
&lt;li&gt;Running openshift-install to generate manifests.&lt;/li&gt;
&lt;li&gt;Critical Step: Disabling Secure Boot on VMs (the "secret" to getting iPXE to work).&lt;/li&gt;
&lt;li&gt;Powering on the nodes and watching the magic happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

&lt;p&gt;Did you find this guide helpful? Let me know in the comments if you'd prefer using Ansible for the setup instead of manual commands! Also, subscribe to follow the rest of the series.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>openshift</category>
      <category>infrastructure</category>
      <category>redhat</category>
    </item>
    <item>
      <title>Building a Kubernetes Cluster on Red Hat Enterprise Linux 10: A kubeadm Guide</title>
      <dc:creator>Ana Villar</dc:creator>
      <pubDate>Thu, 18 Jun 2026 18:22:11 +0000</pubDate>
      <link>https://dev.to/vilan011/building-a-kubernetes-cluster-on-red-hat-enterprise-linux-10-a-kubeadm-guide-1jpe</link>
      <guid>https://dev.to/vilan011/building-a-kubernetes-cluster-on-red-hat-enterprise-linux-10-a-kubeadm-guide-1jpe</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this post, I'll walk you through deploying a production-ready Kubernetes cluster on Red Hat Enterprise Linux 10 using kubeadm. This lab was inspired by Anthony E. Nocentino's excellent &lt;a href="https://app.pluralsight.com/ilx/video-courses/cka-kubeadm-install-basic-cluster-using-cert/course-overview" rel="noopener noreferrer"&gt;Certified Kubernetes Administrator (CKA): Using kubadm to Install a Basic Cluster&lt;/a&gt; training course, which is part of the official &lt;a href="https://app.pluralsight.com/paths/skills/certified-kubernetes-administrator" rel="noopener noreferrer"&gt;Certified Kubernetes Administrator (CKA) path&lt;/a&gt; on &lt;a href="https://www.pluralsight.com/" rel="noopener noreferrer"&gt;Pluralsight&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;⭐ Shout-out: Anthony is a fantastic trainer! His course uses Ubuntu 22.04 as the base OS. I adapted his approach to work on RHEL 10, adding some additional considerations specific to Red Hat's ecosystem.&lt;/p&gt;

&lt;p&gt;One intentional decision in this setup: I deployed Kubernetes v1.35 and CRI-O v1.35, which wasn't the latest version available at installation time. &lt;/p&gt;

&lt;p&gt;This was purposeful. Anthony's course includes a dedicated section on upgrading clusters, and using a slightly older baseline makes that learning path clearer. &lt;/p&gt;

&lt;p&gt;The upgrade procedures (not covered here) are what really solidify your understanding of cluster lifecycle management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab Infrastructure Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Nodes Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;vCPUs&lt;/th&gt;
&lt;th&gt;IP Address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rh-cp1&lt;/td&gt;
&lt;td&gt;Control Plane&lt;/td&gt;
&lt;td&gt;12 GiB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;192.168.110.120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rh-node1&lt;/td&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;6 GiB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;192.168.110.121&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rh-node2&lt;/td&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;6 GiB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;192.168.110.122&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rh-node3&lt;/td&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;6 GiB&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;192.168.110.123&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: The IP address schema is just an example and what was more convenient for me.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting Infrastructure
&lt;/h3&gt;

&lt;p&gt;A dedicated &lt;em&gt;utilities&lt;/em&gt; VM (also RHEL 10) provides essential services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS (BIND/named)&lt;/li&gt;
&lt;li&gt;NTP (chrony)&lt;/li&gt;
&lt;li&gt;HTTP (Apache/httpd)&lt;/li&gt;
&lt;li&gt;DHCP (Kea)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This centralized infrastructure simplifies name resolution across all cluster nodes. But this is not essential for this project. You can, instead, ensure the nodes are able to reach each other updating the file &lt;em&gt;/etc/hosts&lt;/em&gt; on all nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites &amp;amp; OS Preparation
&lt;/h2&gt;

&lt;p&gt;Before diving into Kubernetes, we need consistent node preparation across &lt;strong&gt;all machines&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. System Registration and Updates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;subscription-manager register &lt;span class="nt"&gt;--username&lt;/span&gt; &amp;lt;username&amp;gt; &lt;span class="nt"&gt;--password&lt;/span&gt; &amp;lt;password&amp;gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf update redhat-release
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf upgrade
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Disable Swap (Required by Kubernetes)
&lt;/h3&gt;

&lt;p&gt;Edit &lt;em&gt;/etc/fstab&lt;/em&gt; to comment out swap entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;=&lt;span class="n"&gt;xxxxxxxx&lt;/span&gt;-&lt;span class="n"&gt;xxx&lt;/span&gt;-&lt;span class="n"&gt;xxxx&lt;/span&gt;-&lt;span class="n"&gt;xxxx&lt;/span&gt;-&lt;span class="n"&gt;xxxxxxxxxxxx&lt;/span&gt; &lt;span class="n"&gt;none&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c"&gt;# ^ Comment this line out
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;swapoff &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Disable Firewalld
&lt;/h3&gt;

&lt;p&gt;Disable firewalld, as indicated in the &lt;a href="https://docs.tigera.io/calico/latest/getting-started/kubernetes/requirements#node-requirements" rel="noopener noreferrer"&gt;Calico System requirements for Kubernetes&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop firewalld
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable firewalld
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl mask firewalld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Production Note: Use Calico to maintaining security and enforce network policies later.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Load Kernel Modules and Enable IP Forwarding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe overlay
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe br_netfilter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure sysctl parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo 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
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the modules loaded correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;lsmod | &lt;span class="nb"&gt;grep &lt;/span&gt;overlay
&lt;span class="nv"&gt;$ &lt;/span&gt;lsmod | &lt;span class="nb"&gt;grep &lt;/span&gt;br_netfilter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installing Kubernetes Components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setting Version Variables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ KUBERNETES_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.35
&lt;span class="nv"&gt;$ CRIO_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.35
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding Repositories
&lt;/h3&gt;

&lt;p&gt;Create the Kubernetes repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/&lt;/span&gt;&lt;span class="nv"&gt;$KUBERNETES_VERSION&lt;/span&gt;&lt;span class="sh"&gt;/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/&lt;/span&gt;&lt;span class="nv"&gt;$KUBERNETES_VERSION&lt;/span&gt;&lt;span class="sh"&gt;/rpm/repodata/repomd.xml.key
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the CRI-O repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/yum.repos.d/cri-o.repo
[cri-o]
name=CRI-O
baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/&lt;/span&gt;&lt;span class="nv"&gt;$CRIO_VERSION&lt;/span&gt;&lt;span class="sh"&gt;/rpm/
enabled=1
gpgcheck=1
gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/&lt;/span&gt;&lt;span class="nv"&gt;$CRIO_VERSION&lt;/span&gt;&lt;span class="sh"&gt;/rpm/repodata/repomd.xml.key
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; kubelet kubeadm kubectl cri-o container-selinux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuring CRI-O
&lt;/h3&gt;

&lt;p&gt;Create a cgroup manager configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/crio/crio.conf.d/02-cgroup-manager.conf
[crio.runtime]
conmon_cgroup = "pod"
cgroup_manager = "cgroupfs"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; crio kubelet
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart crio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Version Locking
&lt;/h3&gt;

&lt;p&gt;To prevent accidental upgrades:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'dnf-command(versionlock)'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf versionlock add kubeadm-1.35.4 kubelet-1.35.4 kubectl-1.35.4 cri-o-1.35.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Review the output from the installation of the packages kubeadm, kubelet, kubectl and cri-o, and update the versions to lock in the command above. &lt;/p&gt;

&lt;h2&gt;
  
  
  Initializing the Control Plane
&lt;/h2&gt;

&lt;p&gt;On rh-cp1, download and configure Calico networking. To know the current lastest version, check &lt;a href="https://docs.tigera.io/calico/latest/getting-started/kubernetes/self-managed-onprem/onpremises" rel="noopener noreferrer"&gt;Tigera documentation&lt;/a&gt;, in the Manifest tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wget https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/calico.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the CALICO_IPV4POOL_CIDR value to match your pod network plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CALICO_IPV4POOL_CIDR&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.244.0.0/16"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize the cluster, to use the same subnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm init &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kubernetes-version&lt;/span&gt; v1.35.4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pod-network-cidr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.244.0.0/16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cri-socket&lt;/span&gt; unix:///var/run/crio/crio.sock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upload-certs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once successful, save the join commands that appear at the end of the output—you'll need these for worker nodes!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;To start using your cluster, you need to run the following as a regular user:

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

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.110.120:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
 --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Kubernetes control-plane has initialized successfully!&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring kubectl
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; /etc/kubernetes/admin.conf &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy Calico Network
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; calico.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait a few minutes and verify pods are running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pods --all-namespaces
NAMESPACE     NAME                                       READY   STATUS    RESTARTS   AGE
kube-system   calico-kube-controllers-6b4b6457d5-p98c2   1/1     Running   0          2m47s
kube-system   calico-node-5vjrr                          1/1     Running   0          2m47s
kube-system   coredns-7d764666f9-r4vqp                   1/1     Running   0          4m9s
kube-system   coredns-7d764666f9-vh7df                   1/1     Running   0          4m9s
kube-system   etcd-rh-cp1                                1/1     Running   0          4m28s
kube-system   kube-apiserver-rh-cp1                      1/1     Running   0          4m28s
kube-system   kube-controller-manager-rh-cp1             1/1     Running   0          4m27s
kube-system   kube-proxy-4r6h8                           1/1     Running   0          4m10s
kube-system   kube-scheduler-rh-cp1                      1/1     Running   0          4m28s
$ kubectl get nodes
NAME     STATUS   ROLES           AGE     VERSION
rh-cp1   Ready    control-plane   4m40s   v1.35.4
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Joining Worker Nodes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On each worker node&lt;/strong&gt; (rh-node1, rh-node2, rh-node3), run the join command saved during kubeadm init:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm &lt;span class="nb"&gt;join &lt;/span&gt;192.168.110.120:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;token&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--discovery-token-ca-cert-hash&lt;/span&gt; sha256:&amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the cluster health &lt;strong&gt;from the control plane&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All nodes should show Ready status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get nodes
NAME       STATUS   ROLES           AGE     VERSION
rh-cp1     Ready    control-plane   10m     v1.35.4
rh-node1   Ready    &amp;lt;none&amp;gt;          2m11s   v1.35.4
rh-node2   Ready    &amp;lt;none&amp;gt;          113s    v1.35.4
rh-node3   Ready    &amp;lt;none&amp;gt;          102s    v1.35.4
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install bash completion for kubectl:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;bash-completion
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"source &amp;lt;(kubectl completion bash)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing the Deployment
&lt;/h2&gt;

&lt;p&gt;Deploy a test application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl create deployment hello-world &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;psk8s.azurecr.io/hello-app:1.0
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get pods &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expose it via a service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl expose deployment hello-world &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 &lt;span class="nt"&gt;--target-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8080
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get service hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get service hello-world
NAME          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
hello-world   ClusterIP   10.99.168.218   &amp;lt;none&amp;gt;        80/TCP    8s
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, test it. You should see a response from your running container!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl http://10.99.168.218:80
Hello, world!
Version: 1.0.0
hello-world-b5b7f67cc-d26dt
$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean up after testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl delete service hello-world
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl delete deployment hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Considerations When Moving from Ubuntu to RHEL
&lt;/h2&gt;

&lt;p&gt;Here are the main differences I encountered adapting Anthony's tutorial:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Ubuntu Approach&lt;/th&gt;
&lt;th&gt;RHEL 10 Adaptation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Package Manager&lt;/td&gt;
&lt;td&gt;apt/dpkg&lt;/td&gt;
&lt;td&gt;dnf/rpm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall Management&lt;/td&gt;
&lt;td&gt;ufw/firewalld optional&lt;/td&gt;
&lt;td&gt;firewalld disabled (use Calico policies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;subscription-manager required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELinux&lt;/td&gt;
&lt;td&gt;Permissive mode default&lt;/td&gt;
&lt;td&gt;Need to handle SELinux context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRI Runtime&lt;/td&gt;
&lt;td&gt;containerd&lt;/td&gt;
&lt;td&gt;CRI-O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This setup provides a solid foundation for learning Kubernetes administration. From here, you could explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cluster upgrades (covered extensively in Anthony's course)&lt;/li&gt;
&lt;li&gt;Network policy enforcement with Calico&lt;/li&gt;
&lt;li&gt;High availability with multiple control plane nodes&lt;/li&gt;
&lt;li&gt;Storage classes and persistent volumes&lt;/li&gt;
&lt;li&gt;Monitoring stack with Prometheus/Grafana&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this walkthrough helpful, I'd highly recommend checking out the original Pluralsight course. Anthony's explanations are crystal clear, and adapting them to different distributions is an excellent way to deepen your understanding of what happens under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://app.pluralsight.com/paths/skills/certified-kubernetes-administrator" rel="noopener noreferrer"&gt;Pluralsight: Certified Kubernetes Administrator Certification Path&lt;/a&gt;&lt;br&gt;
&lt;a href="https://kubernetes.io/docs/" rel="noopener noreferrer"&gt;Official Kubernetes Documentation&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/projectcalico/calico" rel="noopener noreferrer"&gt;Calico Project GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! Feel free to share your own experiences with Kubernetes on RHEL in the comments below.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>rhel</category>
      <category>kubeadm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Complete Guide: Setting Up XWiki 17 on Premises</title>
      <dc:creator>Ana Villar</dc:creator>
      <pubDate>Fri, 12 Jun 2026 19:04:27 +0000</pubDate>
      <link>https://dev.to/vilan011/complete-guide-setting-up-xwiki-17-on-premises-4coo</link>
      <guid>https://dev.to/vilan011/complete-guide-setting-up-xwiki-17-on-premises-4coo</guid>
      <description>&lt;p&gt;This guide walks through deploying XWiki 17.10.8 on Red Hat Enterprise Linux 10.2 with Apache Tomcat and PostgreSQL. Total setup time: ~45 minutes. Requires minimum 8GB RAM and 4 vCPUs for acceptable performance.&lt;/p&gt;

&lt;p&gt;If you're looking to deploy a self-hosted wiki solution with enterprise-grade features, XWiki is an excellent choice. Unlike some alternatives, it offers powerful permission management, structured data, and extensibility through extensions. However, the on-premises setup can be intimidating due to multiple moving parts.&lt;/p&gt;

&lt;p&gt;This guide details setting up XWiki 17 on Red Hat Linux Server 10.2 with 16GB RAM and 4 vCPUs. I initially validated these steps in a Mac-hosted VM before deploying them to my production environment on a Dell OptiPlex 7050 Micro.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hardware Requirements
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Minimum&lt;/th&gt;
&lt;th&gt;Recommended&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;2 cores&lt;/td&gt;
&lt;td&gt;4+ cores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;4 GB&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;20 GB&lt;/td&gt;
&lt;td&gt;100 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; During my initial setup on a 2GB RAM old laptop, XWiki was functional but painfully slow. The jump to 16GB made a night-and-day difference in responsiveness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Operating System: Red Hat Enterprise Linux 10.2 or Fedora Server &lt;/li&gt;
&lt;li&gt;Servlet Container: Apache Tomcat 10.1.x (Jetty is also supported) &lt;/li&gt;
&lt;li&gt;Database: PostgreSQL 16.x&lt;/li&gt;
&lt;li&gt;Java: OpenJDK 21&lt;/li&gt;
&lt;li&gt;XWiki Platform: 17.10.8 WAR package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As Operating System, you can use other Linux Distributions, including Ubuntu, Windows Server and Docker Engine.&lt;/li&gt;
&lt;li&gt;As Servlet Container, Jetty is also a supported option.&lt;/li&gt;
&lt;li&gt;Other supported databases are HyperSQL, MariaDB, MySQL, or Oracle.&lt;/li&gt;
&lt;li&gt;For more information, review &lt;a href="https://www.xwiki.org/xwiki/bin/view/documentation/xs/admin/installation/prerequisites/" rel="noopener noreferrer"&gt;XWiki prerequisites&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Time Investment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Installation: ~30 minutes&lt;/li&gt;
&lt;li&gt;Configuration: ~15 minutes&lt;/li&gt;
&lt;li&gt;Testing: ~10 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preparation Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install Your Base OS
&lt;/h3&gt;

&lt;p&gt;For this guide, I'm using Red Hat Enterprise Linux, but most steps work equally well on Fedora Server if you don't have RHEL subscriptions.&lt;/p&gt;

&lt;p&gt;Download from &lt;a href="https://developers.redhat.com/" rel="noopener noreferrer"&gt;Red Hat Developer Program&lt;/a&gt; or &lt;a href="https://fedoraproject.org/server/download/" rel="noopener noreferrer"&gt;Fedora Server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once installed, register (if using RHEL), update, and reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;subscription-manager register &lt;span class="nt"&gt;--username&lt;/span&gt; &amp;lt;username&amp;gt; &lt;span class="nt"&gt;--password&lt;/span&gt; &amp;lt;password&amp;gt;
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
~&lt;span class="nv"&gt;$ &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Dependencies
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Install and configure Apache Tomcat
&lt;/h4&gt;

&lt;p&gt;Tomcat serves as our servlet container for hosting XWiki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;tomcat &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command pulls in Java 21 along with necessary dependencies automatically. Review the installation output to confirm java-21-openjdk-headless was included.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Architecture Note&lt;/em&gt;: &lt;br&gt;
Package names vary by architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;x86_64: java-21-openjdk-headless.x86_64&lt;/li&gt;
&lt;li&gt;aarch64: java-21-openjdk-headless.aarch64&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it seems to be missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;java-21-openjdk-headless.x86_64 &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;java-21-openjdk-headless.aarch64 &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Configure Firewall
&lt;/h5&gt;

&lt;p&gt;Allow HTTP traffic on port 8080 (Tomcat's default):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8080/tcp
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it's open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--list-all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Configure Tomcat Memory Settings
&lt;/h5&gt;

&lt;p&gt;By default, Tomcat won't allocate enough memory for XWiki to perform well. Create a custom setenv.sh file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/tomcat/bin
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;tomcat:tomcat &lt;span class="nt"&gt;-R&lt;/span&gt; /var/lib/tomcat
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /var/lib/tomcat/bin/setenv.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JAVA_OPTS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -Djava.awt.headless=true"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CATALINA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-server -Xms1080m -Xmx1600m -Dfile.encoding=utf-8 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
-Djava.awt.headless=true &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
--add-opens java.base/java.lang=ALL-UNNAMED &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
--add-opens java.base/java.io=ALL-UNNAMED &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
--add-opens java.base/java.util=ALL-UNNAMED &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
--add-opens java.base/java.util.concurrent=ALL-UNNAMED"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Memory Explanation&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-Xms&lt;/td&gt;
&lt;td&gt;1080MB&lt;/td&gt;
&lt;td&gt;Initial heap size (prevents startup allocation spikes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-Xmx&lt;/td&gt;
&lt;td&gt;1600MB&lt;/td&gt;
&lt;td&gt;Maximum heap size (prevents runaway memory consumption)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--add-opens&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Required for Java 17+ compatibility with certain libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set ownership and test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;tomcat:tomcat /var/lib/tomcat/bin/setenv.sh
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; tomcat
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test from another machine by visiting &lt;a href="http://&lt;your-server-name&gt;:8080" rel="noopener noreferrer"&gt;http://your-server-name:8080&lt;/a&gt;. You should see the Tomcat welcome page. Once confirmed, stop the service — we'll restart after XWiki deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Database Configuration (PostgreSQL)
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Install PostgreSQL
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;postgresql-server postgresql-contrib &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize and Start PostgreSQL&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postgresql-setup &lt;span class="nt"&gt;--initdb&lt;/span&gt;
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; postgresql
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Create XWiki User and Database
&lt;/h5&gt;

&lt;p&gt;Switch to the postgres user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su postgres
bash-5.2&lt;span class="nv"&gt;$ &lt;/span&gt;psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run these SQL commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;xwiki&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'xwikipassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;xwiki&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;OWNER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;xwiki&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;xwiki&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Security Warning: Replace 'xwikipassword' with a strong, unique password. Never use default credentials in production. Consider using environment variables or secret management tools like HashiCorp Vault for sensitive data.&lt;/p&gt;

&lt;h5&gt;
  
  
  Configure PostgreSQL Authentication
&lt;/h5&gt;

&lt;p&gt;Find the authentication config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su postgres
bash-5.2&lt;span class="nv"&gt;$ &lt;/span&gt;psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SHOW hba_file;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the file and location is &lt;em&gt;/var/lib/pgsql/data/pg_hba.conf&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Edit the pg_hba.conf file to change authentication method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /var/lib/pgsql/data/pg_hba.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# OLD (insecure)
&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;             &lt;span class="n"&gt;all&lt;/span&gt;             &lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;/&lt;span class="m"&gt;32&lt;/span&gt;            &lt;span class="n"&gt;ident&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;             &lt;span class="n"&gt;all&lt;/span&gt;             ::&lt;span class="m"&gt;1&lt;/span&gt;/&lt;span class="m"&gt;128&lt;/span&gt;                 &lt;span class="n"&gt;ident&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# NEW (secure)
&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;             &lt;span class="n"&gt;all&lt;/span&gt;             &lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;/&lt;span class="m"&gt;32&lt;/span&gt;            &lt;span class="n"&gt;scram&lt;/span&gt;-&lt;span class="n"&gt;sha&lt;/span&gt;-&lt;span class="m"&gt;256&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;    &lt;span class="n"&gt;all&lt;/span&gt;             &lt;span class="n"&gt;all&lt;/span&gt;             ::&lt;span class="m"&gt;1&lt;/span&gt;/&lt;span class="m"&gt;128&lt;/span&gt;                 &lt;span class="n"&gt;scram&lt;/span&gt;-&lt;span class="n"&gt;sha&lt;/span&gt;-&lt;span class="m"&gt;256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload PostgreSQL to apply changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify Database Connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;psql postgresql://xwiki:xwikipassword@localhost
&lt;span class="nv"&gt;xwiki&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\l&lt;/span&gt;  &lt;span class="c"&gt;# Should list databases including 'xwiki'&lt;/span&gt;
&lt;span class="nv"&gt;xwiki&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\q&lt;/span&gt;  &lt;span class="c"&gt;# Exit psql&lt;/span&gt;
~&lt;span class="err"&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploying XWiki
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Download Required Files
&lt;/h3&gt;

&lt;p&gt;Get these two files from official sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XWiki WAR Package: &lt;a href="https://www.xwiki.org/xwiki/bin/view/Download" rel="noopener noreferrer"&gt;Production Installation&lt;/a&gt; → Select "WAR Package for Servlet Container"&lt;/li&gt;
&lt;li&gt;PostgreSQL JDBC Driver: &lt;a href="https://jdbc.postgresql.org/download/" rel="noopener noreferrer"&gt;Official Downloads&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Place them in a download directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/downloads
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/downloads
&lt;span class="c"&gt;# wget the links here from browser or CLI&lt;/span&gt;
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;downloads/
postgresql-42.7.11.jar  xwiki-platform-distribution-war-17.10.8.jar
~&lt;span class="nv"&gt;$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy the WAR Package
&lt;/h3&gt;

&lt;p&gt;Copy and extract the XWiki WAR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; ~/downloads/xwiki-platform-distribution-war-17.10.8.jar /var/lib/tomcat/webapps/
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/lib/tomcat/webapps
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;unzip xwiki-platform-distribution-war-17.10.8.jar
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir &lt;/span&gt;xwiki
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mv &lt;/span&gt;META-INF redirect resources skins templates WEB-INF xwiki/
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo rm &lt;/span&gt;xwiki-platform-distribution-war-17.10.8.jar
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; tomcat:tomcat xwiki/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common Pitfall: If you get an error about moving files into themselves, ensure you're not inside the extracted directory when creating the target folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add PostgreSQL JDBC Driver
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/lib/tomcat/webapps/xwiki/WEB-INF/lib
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; ~/downloads/postgresql-42.7.11.jar &lt;span class="nb"&gt;.&lt;/span&gt;
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;tomcat:tomcat &lt;span class="k"&gt;*&lt;/span&gt;.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure Hibernate for PostgreSQL
&lt;/h3&gt;

&lt;p&gt;Edit the Hibernate configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/lib/tomcat/webapps/xwiki/WEB-INF
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano hibernate.cfg.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find and comment out the default database section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;    &lt;span class="c"&gt;&amp;lt;!--
    &amp;lt;property name="hibernate.connection.url"&amp;gt;jdbc:hsqldb:file:${environment.permanentDirectory}/database/xwiki_db;shutdown=true&amp;lt;/property&amp;gt;
    &amp;lt;property name="hibernate.connection.username"&amp;gt;sa&amp;lt;/property&amp;gt;
    &amp;lt;property name="hibernate.connection.password"&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;property name="hibernate.connection.driver_class"&amp;gt;org.hsqldb.jdbcDriver&amp;lt;/property&amp;gt;
    --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find and uncomment the PostgreSQL section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;    &lt;span class="c"&gt;&amp;lt;!-- PostgreSQL configuration.
         Uncomment if you want to use PostgreSQL and comment out other database configurations.
         Notes:
           - "hibernate.jdbc.use_streams_for_binary" needs to be set to "false",
             see https://community.jboss.org/wiki/HibernateCoreMigrationGuide36
           - "xwiki.virtual_mode" can be set to either "schema" or "database". Note that currently the database mode
             doesn't support database creation (see https://jira.xwiki.org/browse/XWIKI-8753)
           - if you want the main wiki database to be different than "xwiki" (or "public" in schema mode)
             you will also have to set the property xwiki.db in xwiki.cfg file
    --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.url"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;jdbc:postgresql://localhost:5432/xwiki&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.username"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;xwiki&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.password"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;**xwikipassword**&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.driver_class"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;org.postgresql.Driver&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.jdbc.use_streams_for_binary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"xwiki.virtual_mode"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;schema&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.charSet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;UTF-8&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.useUnicode"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.characterEncoding"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;utf8&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;mapping&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"xwiki.postgresql.hbm.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mapping&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"feeds.hbm.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mapping&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"instance.hbm.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mapping&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"notification-filter-preferences.hbm.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;mapping&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"mailsender.hbm.xml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;


    &lt;span class="c"&gt;&amp;lt;!-- Oracle configuration.
...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Password Reminder: Update the password value to match what you set earlier!&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Permanent Directory
&lt;/h3&gt;

&lt;p&gt;Create a dedicated directory for XWiki data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/xwiki/data
~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; tomcat:tomcat /var/lib/xwiki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Edit the properties file:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano /var/lib/tomcat/webapps/xwiki/WEB-INF/xwiki.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uncomment and verify this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;environment.permanentDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/lib/xwiki/data/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Starting the Application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait 3-5 minutes for Tomcat to fully initialize XWiki (it compiles resources on first boot). Monitor logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/tomcat/catalina.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for messages like XWiki context has been initialized successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  XWiki Configuration Wizard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Running the Configuration Wizard
&lt;/h3&gt;

&lt;p&gt;Navigate to your server: &lt;a href="http://&lt;your-server-name&gt;:8080/xwiki" rel="noopener noreferrer"&gt;http://your-server-name:8080/xwiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You'll see an initialization banner briefly, then the wizard will appear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmvkp22ygjb44zleqi33e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmvkp22ygjb44zleqi33e.png" alt="XWiki initialization banner" width="751" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Wizard Steps
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqlxt96aqqys6y1d8wmr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqlxt96aqqys6y1d8wmr.png" alt="Distribution wizard" width="629" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Continue&lt;/strong&gt; to begin&lt;/p&gt;

&lt;p&gt;Create Administrator Account: Set a strong admin password (save this securely!).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiawcsqmulnlfo4ubn4on.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiawcsqmulnlfo4ubn4on.png" alt="Create Admin User" width="800" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Register and Login&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvbuoc1po2jyga8z0er2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvbuoc1po2jyga8z0er2.png" alt="User created" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Continue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install Standard Flavor: Select "XWiki Standard Flavor" from the dropdown—this includes useful built-in applications&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxtpv1em9qxsii6t2li93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxtpv1em9qxsii6t2li93.png" alt="Standard Flavor" width="800" height="763"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Install this flavor&lt;/strong&gt;: Wait 2-5 minutes depending on hardware&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztbtxbuv8616c6wgvqdm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztbtxbuv8616c6wgvqdm.png" alt="Flavor deployment" width="799" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Install&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevnguhdox2d17n5aa5y6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevnguhdox2d17n5aa5y6.png" alt=" " width="800" height="762"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you see it has been Installed, Press &lt;strong&gt;Continue&lt;/strong&gt; to completion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3szhx03h76tfaxpejqyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3szhx03h76tfaxpejqyf.png" alt="Deployment completed" width="800" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After finalization, press Continue and you'll be redirected to your XWiki home page with example documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fup5shavbx1toog7rx1z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fup5shavbx1toog7rx1z5.png" alt="XWiki Home Page" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your XWiki server is ready!&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Installation Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Security Hardening
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Command/File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Change default PostgreSQL password&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;psql ALTER USER command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update Tomcat manager passwords&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;tomcat-users.xml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable HTTPS via reverse proxy&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;nginx/Apache configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restrict database access&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;pg_hba.conf network rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regular backups&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;XWiki GUI + cron jobs + pg_dump&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Performance Tuning
&lt;/h3&gt;

&lt;p&gt;With 16GB RAM, you may want to increase XWiki's maximum heap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Edit setenv.sh&lt;/span&gt;
&lt;span class="nv"&gt;CATALINA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx2048m ..."&lt;/span&gt;  &lt;span class="c"&gt;# Increase if needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable query caching in xwiki.properties:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;cache.default.maxSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backup Strategy
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Using XWiki GUI
&lt;/h4&gt;

&lt;p&gt;Go to Administer Wiki:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jwh7usbsqsfjtqpoic7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jwh7usbsqsfjtqpoic7.png" alt="Administer Wiki" width="705" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select Content:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrlgjuwv3l5lqq3bvynm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrlgjuwv3l5lqq3bvynm.png" alt="Administer Options" width="800" height="649"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And select Export:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4wffbi67ehiepznbf2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4wffbi67ehiepznbf2q.png" alt="Option Export" width="800" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can select what pages to export/backup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijt941276t7tn0wx75ut.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijt941276t7tn0wx75ut.PNG" alt="Export" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Database:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; xwiki xwiki &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; xwiki_backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  File Data:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tar &lt;/span&gt;czf xwiki_files_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz /var/lib/xwiki/data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule these as cron jobs for automated backups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: XWiki Won't Load After Startup
&lt;/h3&gt;

&lt;p&gt;Symptoms: Browser shows blank page or timeout at /xwiki&lt;/p&gt;

&lt;p&gt;Solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check logs: &lt;code&gt;tail -f /var/log/tomcat/catalina.out&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verify database connection is active&lt;/li&gt;
&lt;li&gt;Ensure hibernate.cfg.xml has correct credentials&lt;/li&gt;
&lt;li&gt;Restart Tomcat completely&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Issue: Slow Page Loading
&lt;/h3&gt;

&lt;p&gt;Symptoms: Pages take 5+ seconds to load&lt;/p&gt;

&lt;p&gt;Solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increase JVM heap in setenv.sh&lt;/li&gt;
&lt;li&gt;Check available RAM with free -h&lt;/li&gt;
&lt;li&gt;Disable unnecessary extensions in XWiki admin panel&lt;/li&gt;
&lt;li&gt;Consider upgrading to SSD storage&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Issue: Database Connection Error
&lt;/h3&gt;

&lt;p&gt;Symptoms: "Cannot connect to database" during wizard&lt;/p&gt;

&lt;p&gt;Solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify PostgreSQL is running: &lt;code&gt;sudo systemctl status postgresql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test connectivity: &lt;code&gt;psql -U xwiki -d xwiki&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check pg_hba.conf has scram-sha-256 authentication&lt;/li&gt;
&lt;li&gt;Ensure firewall allows localhost PostgreSQL connections (port 5432)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why On-Premises Over Cloud?
&lt;/h2&gt;

&lt;p&gt;You might wonder why choose self-hosted instead of XWiki Cloud. Here are compelling reasons:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;On-Premises&lt;/th&gt;
&lt;th&gt;Cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Control&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Limited by provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Restricted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost (Long-term)&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;Subscription-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline Access&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Requires internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Direct system access&lt;/td&gt;
&lt;td&gt;API-only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For organizations handling sensitive data or requiring deep integration, on-premises deployment provides the flexibility you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Setting up XWiki on-premises involves coordinating several components, but once configured, you have a powerful, self-contained knowledge management platform. The setup I've outlined balances security, performance, and maintainability.&lt;/p&gt;

&lt;p&gt;Got questions or encountered different errors? Drop a comment below — I'm happy to help troubleshoot!&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xwiki.org/xwiki/bin/view/Documentation/AdminGuide/Installation/InstallationWAR/" rel="noopener noreferrer"&gt;Installing XWiki .war package manually&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xwiki.org/xwiki/bin/view/ReleaseNotes/Data/XWiki/17.10.8/" rel="noopener noreferrer"&gt;Release Notes for XWiki 17.10.8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xwiki.org/xwiki/bin/view/Documentation/AdminGuide/" rel="noopener noreferrer"&gt;Administrator Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>xwiki</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Hello, DEV Community! 👋</title>
      <dc:creator>Ana Villar</dc:creator>
      <pubDate>Sat, 30 May 2026 15:53:23 +0000</pubDate>
      <link>https://dev.to/vilan011/hello-dev-community-4i30</link>
      <guid>https://dev.to/vilan011/hello-dev-community-4i30</guid>
      <description>&lt;p&gt;I'm Ana, and I'm excited to start sharing my technical journey here. This is a brief post to introduce myself and give you a heads-up on what's coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Expect
&lt;/h2&gt;

&lt;p&gt;In upcoming posts, I'll be diving into hands-on, infrastructure-focused tutorials and walkthroughs, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setting up a XWiki server on-premises&lt;/strong&gt; — from installation to configuration, getting a collaborative wiki platform running in your own environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploying Kubernetes on RHEL 10 virtual machines&lt;/strong&gt; — step-by-step guidance on building a Kubernetes cluster on Red Hat Enterprise Linux 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploying OpenShift as virtual machines on a RHEL 10 host&lt;/strong&gt; — exploring how to run OpenShift on top of RHEL 10, combining the power of containers with VM-level control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My goal is to keep things practical, clear, and rooted in real-world experience — the kind of content I wish I'd had when tackling these setups myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why These Topics?
&lt;/h2&gt;

&lt;p&gt;Because on-premises infrastructure is far from dead. Whether it's compliance requirements, performance needs, or simply wanting full control over your stack, there's still a strong case for running things yourself. And RHEL was my choice for certification purposes, and it's given me the flexibility to keep using hardware that other vendors have discontinued.&lt;/p&gt;

&lt;p&gt;Stay tuned, and feel free to follow me so you don't miss the upcoming posts. If any of these topics spark your interest, I'd love to hear about it in the comments! 🚀&lt;/p&gt;

</description>
      <category>redhat</category>
      <category>kubernetes</category>
      <category>onprem</category>
    </item>
  </channel>
</rss>
