<?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>Forem: William Kwabena Akoto</title>
    <description>The latest articles on Forem by William Kwabena Akoto (@kobbyprincee).</description>
    <link>https://forem.com/kobbyprincee</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3105309%2F85f80554-8bed-4fbd-8d4e-81864e0791fd.png</url>
      <title>Forem: William Kwabena Akoto</title>
      <link>https://forem.com/kobbyprincee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kobbyprincee"/>
    <language>en</language>
    <item>
      <title>Dual-Booting Windows 11 &amp; Rocky Linux — A Beginner's Complete Guide</title>
      <dc:creator>William Kwabena Akoto</dc:creator>
      <pubDate>Sun, 03 May 2026 22:12:17 +0000</pubDate>
      <link>https://forem.com/kobbyprincee/dual-booting-windows-11-rocky-linux-a-beginners-complete-guide-533c</link>
      <guid>https://forem.com/kobbyprincee/dual-booting-windows-11-rocky-linux-a-beginners-complete-guide-533c</guid>
      <description>&lt;p&gt;A step-by-step guide for beginners who want a gaming PC &lt;em&gt;and&lt;/em&gt; a real enterprise Linux environment on the same machine — with every decision explained in plain English.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What Is Dual-Booting and Why Rocky Linux?&lt;/li&gt;
&lt;li&gt;
Key Concepts You Must Understand First

&lt;ul&gt;
&lt;li&gt;UEFI, BIOS, and Secure Boot&lt;/li&gt;
&lt;li&gt;Partitions, File Systems, and GPT&lt;/li&gt;
&lt;li&gt;The GRUB Bootloader&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Before You Begin — Checklist&lt;/li&gt;
&lt;li&gt;Phase 1 — Shrink Your Windows Partition&lt;/li&gt;
&lt;li&gt;Phase 2 — Download &amp;amp; Flash Rocky Linux&lt;/li&gt;
&lt;li&gt;Phase 3 — Configure the HP BIOS&lt;/li&gt;
&lt;li&gt;Phase 4 — Boot the Rocky Linux Installer&lt;/li&gt;
&lt;li&gt;Phase 5 — Anaconda Installer Walkthrough&lt;/li&gt;
&lt;li&gt;Phase 6 — First Boot &amp;amp; the GRUB Menu&lt;/li&gt;
&lt;li&gt;Phase 7 — Post-Install DevOps Setup&lt;/li&gt;
&lt;li&gt;Troubleshooting Common Problems&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. What Is Dual-Booting and Why Rocky Linux?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dual-booting&lt;/strong&gt; means installing two separate operating systems on the same physical computer, each living in its own isolated section of the hard drive called a &lt;em&gt;partition&lt;/em&gt;. When you power on your laptop, a small program called a &lt;strong&gt;bootloader&lt;/strong&gt; wakes up and shows you a menu: pick Windows or pick Rocky Linux. The two systems never interfere with each other.&lt;/p&gt;

&lt;p&gt;This is different from a &lt;strong&gt;virtual machine&lt;/strong&gt;, where you'd run Linux inside a window on top of Windows. Dual-booting gives each OS full, direct access to your hardware — better performance, real GPU access for your DevOps tools, and a genuine feel for what it's like to administer a server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Rocky Linux specifically?&lt;/strong&gt; Rocky Linux is a free, community-maintained and designed for 1:1, bug-for-bug downstream binary compatibility with Red Hat Enterprise Linux (RHEL) — the operating system that runs a huge chunk of the world's servers, cloud infrastructure, and enterprise data centres. When companies say they want "Linux experience", they usually mean RHEL-family experience.Rocky Linux is fullRocky Linux 10.1 is fully supported until &lt;strong&gt;May 2035&lt;/strong&gt;, giving you a stable, long-term learning platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rocky Linux will push you further: its default security model (SELinux), firewall tool (firewalld), and package manager (DNF) are all standard in enterprise environments that other linux distributions rarely appear in.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Key Concepts You Must Understand First
&lt;/h2&gt;

&lt;p&gt;Before touching a single setting, you need to understand &lt;em&gt;why&lt;/em&gt; you're making each decision. Skipping this section is how people accidentally wipe their Windows installation. Take ten minutes to read it.&lt;/p&gt;

&lt;h3&gt;
  
  
  UEFI, BIOS, and Secure Boot
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is UEFI / BIOS?&lt;/strong&gt;&lt;br&gt;
UEFI (or its older predecessor, BIOS) is firmware — a tiny program burned into a chip on your motherboard. It's the very first thing that runs when you press the power button, before any operating system loads. Its job is to inventory your hardware (CPU, RAM, drives) and then hand control to a bootloader on one of your storage devices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your computer mostly likely uses &lt;strong&gt;UEFI&lt;/strong&gt;, the modern standard. UEFI uses a special partition called the &lt;strong&gt;EFI System Partition (ESP)&lt;/strong&gt; — a small FAT32 partition that stores bootloader files for every OS installed. Windows already placed its bootloader there. Rocky Linux will add its own file alongside it without deleting Windows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is Secure Boot?&lt;/strong&gt;&lt;br&gt;
Secure Boot is a UEFI feature that checks a digital signature on every bootloader before running it. It prevents malware from hijacking the boot process. However, it can also block Linux installers that aren't signed with a trusted key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rocky Linux does have Secure Boot support, but to avoid any complications on first install, we will &lt;strong&gt;disable Secure Boot&lt;/strong&gt; in the BIOS before installing. You can re-enable it afterwards once everything is working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partitions, File Systems, and GPT
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a partition?&lt;/strong&gt;&lt;br&gt;
Imagine your hard drive as a single large plot of land. A partition is like drawing property lines to divide it into separate sections. Each section is completely independent — one can have Windows on it, another can have Linux, and they don't mix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a file system?&lt;/strong&gt;&lt;br&gt;
A file system is the internal structure that a partition uses to organise files. Windows uses &lt;strong&gt;NTFS&lt;/strong&gt;. Rocky Linux defaults to &lt;strong&gt;XFS&lt;/strong&gt; — the same file system used on most RHEL enterprise servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is GPT?&lt;/strong&gt;&lt;br&gt;
GPT (GUID Partition Table) is the modern standard for how partition information is stored on a disk. Your computer mostly already uses GPT since it might have come with Windows 10/11 already on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is what your disk will look like after the installation is complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ EFI ][ Windows (C:) — ~400 GB ][ /boot ][ swap ][ /  root ][ /home ]
&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;Mount Point&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;File System&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;&lt;code&gt;/boot/efi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reuse existing&lt;/td&gt;
&lt;td&gt;FAT32&lt;/td&gt;
&lt;td&gt;Already exists. Stores bootloaders for all OSes. &lt;strong&gt;Never format.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/boot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;ext4&lt;/td&gt;
&lt;td&gt;Stores the Linux kernel. Kept separate for boot-process compatibility.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;= your RAM size&lt;/td&gt;
&lt;td&gt;swap&lt;/td&gt;
&lt;td&gt;Overflow RAM storage. Required for hibernation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/&lt;/code&gt; (root)&lt;/td&gt;
&lt;td&gt;40–60 GB&lt;/td&gt;
&lt;td&gt;xfs&lt;/td&gt;
&lt;td&gt;The OS, all installed software, and system configs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remaining space&lt;/td&gt;
&lt;td&gt;xfs&lt;/td&gt;
&lt;td&gt;Your personal files and projects. Survives an OS reinstall intact.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The GRUB Bootloader
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is GRUB?&lt;/strong&gt;&lt;br&gt;
GRUB (Grand Unified Bootloader) is the program that Rocky Linux installs to manage booting. After installation, GRUB detects all installed operating systems and shows you a menu. When you select Windows, GRUB hands control to the Windows Boot Manager. When you select Rocky Linux, GRUB loads the Linux kernel directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GRUB installs itself into the EFI System Partition alongside the Windows bootloader. This is why we never format the EFI partition — doing so would delete the Windows bootloader and make Windows unbootable.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Before You Begin — Checklist
&lt;/h2&gt;

&lt;p&gt;Work through this checklist completely before proceeding. Every item matters.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Back up your Windows data first.&lt;/strong&gt; Partitioning operations carry inherent risk. If something goes wrong mid-operation, data loss is possible. Copy your important files to an external drive, a cloud service, or both. This is non-negotiable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Check 1 — CPU Compatibility
&lt;/h3&gt;

&lt;p&gt;Rocky Linux 10 requires a CPU supporting the &lt;strong&gt;x86-64-v3&lt;/strong&gt; instruction set. Any &lt;strong&gt;AMD Ryzen 2000+&lt;/strong&gt; or &lt;strong&gt;Intel 8th Gen (Coffee Lake)+&lt;/strong&gt; processor qualifies.&lt;/p&gt;

&lt;p&gt;To confirm, open PowerShell on Windows and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wmic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check 2 — Disable Windows Fast Startup
&lt;/h3&gt;

&lt;p&gt;Windows Fast Startup leaves the Windows file system in a "partially mounted" state on shutdown. If Linux then tries to read the Windows partition, it can corrupt the file system. Disable it permanently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;Win + S&lt;/code&gt;, search for &lt;strong&gt;Control Panel&lt;/strong&gt;, open it&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Hardware and Sound → Power Options&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Choose what the power buttons do"&lt;/strong&gt; in the left sidebar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Change settings that are currently unavailable"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under "Shutdown settings", &lt;strong&gt;uncheck "Turn on fast startup"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Check 3 — Free Disk Space
&lt;/h3&gt;

&lt;p&gt;Open Disk Management (&lt;code&gt;Win + X&lt;/code&gt; → Disk Management). You need at least &lt;strong&gt;60–80 GB of free space&lt;/strong&gt; on your C: drive to shrink comfortably. You'll be carving that space out as unallocated room for Rocky Linux.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 4 — A 16 GB+ USB Drive
&lt;/h3&gt;

&lt;p&gt;You'll flash the Rocky Linux ISO onto this drive. It will be &lt;strong&gt;completely erased&lt;/strong&gt;. Don't use one with files you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 5 — Stable Power
&lt;/h3&gt;

&lt;p&gt;Plug your laptop in during the entire process. A laptop dying mid-partition or mid-install can corrupt both operating systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1 — Shrink Your Windows Partition
&lt;/h2&gt;

&lt;p&gt;Right now, Windows occupies the entire drive. Before we can install Rocky Linux, we need to shrink the Windows partition and leave behind &lt;strong&gt;unallocated space&lt;/strong&gt; that the Rocky Linux installer will carve into its own partitions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We use Windows' own built-in tool for this — not a third-party app, not the Linux installer. This ensures Windows is aware of the change and updates its own boot records accordingly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How Much Space to Allocate?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&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;Occasional learning / labs&lt;/td&gt;
&lt;td&gt;50 GB&lt;/td&gt;
&lt;td&gt;70 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active DevOps work, Docker, VMs&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;120 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy Kubernetes / container dev&lt;/td&gt;
&lt;td&gt;120 GB&lt;/td&gt;
&lt;td&gt;150+ GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a DevOps workstation, &lt;strong&gt;80–100 GB&lt;/strong&gt; is the sweet spot. Docker images alone can consume 20–30 GB over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Open Disk Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;Win + X&lt;/code&gt; on your keyboard. In the menu that appears, click &lt;strong&gt;Disk Management&lt;/strong&gt;. You'll see a graphical view of your disk with all current partitions shown as coloured bars.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Identify Your C: Drive Partition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the graphical area at the bottom, look for the large partition labelled &lt;strong&gt;(C:)&lt;/strong&gt;. This is your Windows installation. Right-click it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Click "Shrink Volume..."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A dialog box appears. Enter your desired amount in the field "Enter the amount of space to shrink in MB". Convert GB to MB by multiplying by 1024:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;80 GB → enter &lt;strong&gt;81920&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;100 GB → enter &lt;strong&gt;102400&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;120 GB → enter &lt;strong&gt;122880&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Click Shrink and Wait&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Windows will resize the partition (typically 30 seconds to 2 minutes). When done, you'll see a new block of &lt;strong&gt;black/dark unallocated space&lt;/strong&gt; in the disk map. &lt;strong&gt;Do not create a new volume in that space&lt;/strong&gt; — leave it as "Unallocated". The Rocky Linux installer will handle it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If Windows won't let you shrink enough:&lt;/strong&gt; This happens when Windows has "immovable" system files near the end of the partition. Fix: disable hibernation by opening an Administrator Command Prompt and running &lt;code&gt;powercfg /h off&lt;/code&gt;, then try shrinking again.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 2 — Download &amp;amp; Flash Rocky Linux
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 2.1 — Download the ISO
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;ISO file&lt;/strong&gt; is a complete, exact copy of the entire Rocky Linux installer. Go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://rockylinux.org/download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;strong&gt;Rocky Linux 10&lt;/strong&gt;, select the &lt;strong&gt;x86_64&lt;/strong&gt; architecture and download the &lt;strong&gt;DVD ISO&lt;/strong&gt; (~10 GB). Also download the accompanying &lt;strong&gt;CHECKSUM&lt;/strong&gt; file from the same page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2.2 — Verify the ISO (Don't Skip This)
&lt;/h3&gt;

&lt;p&gt;Verifying the checksum confirms the file downloaded completely and without corruption. A partially downloaded or tampered ISO can cause strange installer errors that are very hard to diagnose.&lt;/p&gt;

&lt;p&gt;Open PowerShell and run (adjust the filename to yours):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\Downloads\Rocky-10.1-x86_64-dvd1.iso"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 64-character hash must exactly match the SHA256 value in the CHECKSUM file. If they differ — even by one character — delete the ISO and re-download it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2.3 — Flash the USB Drive with Rufus
&lt;/h3&gt;

&lt;p&gt;Download Rufus from &lt;strong&gt;&lt;a href="https://rufus.ie" rel="noopener noreferrer"&gt;https://rufus.ie&lt;/a&gt;&lt;/strong&gt;. Plug in your 16 GB+ USB drive and open Rufus. Configure it as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rufus Setting&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;Device&lt;/td&gt;
&lt;td&gt;Your USB drive&lt;/td&gt;
&lt;td&gt;Double-check it's the USB, not your internal drive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot selection&lt;/td&gt;
&lt;td&gt;Click SELECT → choose the ISO&lt;/td&gt;
&lt;td&gt;Rufus will auto-detect most settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partition scheme&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required for UEFI systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target system&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UEFI (non CSM)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forces UEFI mode. CSM is a legacy compatibility mode.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File system&lt;/td&gt;
&lt;td&gt;Leave as Rufus default&lt;/td&gt;
&lt;td&gt;Rufus knows best for bootable drives&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;START&lt;/strong&gt;. If a dialog appears, choose &lt;strong&gt;"Write in ISO Image mode"&lt;/strong&gt; and click OK.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The USB drive will be completely wiped.&lt;/strong&gt; All data on the USB will be gone after this step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The flashing process takes 5–15 minutes. When Rufus shows &lt;strong&gt;READY&lt;/strong&gt; in green, the USB is ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3 — Configure the HP BIOS
&lt;/h2&gt;

&lt;p&gt;The BIOS needs to be told two things: disable Secure Boot and boot from the USB instead of the hard drive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Enter the BIOS Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fully &lt;strong&gt;shut down&lt;/strong&gt; your laptop (not restart — shut down). Plug in the Rocky Linux USB. Power on and immediately, repeatedly tap &lt;code&gt;F10&lt;/code&gt; until you see the BIOS setup screen.&lt;/p&gt;

&lt;p&gt;Alternative: power on → tap &lt;code&gt;Esc&lt;/code&gt; → HP startup menu → press &lt;code&gt;F10&lt;/code&gt; to enter Setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Disable Secure Boot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to the &lt;strong&gt;Security&lt;/strong&gt; tab. Find &lt;strong&gt;Secure Boot&lt;/strong&gt; and set it to &lt;strong&gt;Disabled&lt;/strong&gt;. You can re-enable it after installation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Confirm UEFI Boot Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Advanced&lt;/strong&gt; or &lt;strong&gt;Boot Options&lt;/strong&gt;, verify &lt;strong&gt;UEFI Boot Mode&lt;/strong&gt; is selected. Ensure CSM / Legacy Boot is &lt;strong&gt;disabled&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Set USB as First Boot Device&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Boot Order&lt;/strong&gt;. Move the &lt;strong&gt;USB storage device&lt;/strong&gt; to the &lt;strong&gt;top&lt;/strong&gt; of the list using the function keys shown on screen (usually F5/F6 or +/-).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Save and Exit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;F10&lt;/code&gt; to save changes and exit. The laptop will reboot and boot from your Rocky Linux USB.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Alternative: One-time boot menu.&lt;/strong&gt; Power on and press &lt;code&gt;F9&lt;/code&gt; immediately to choose a boot device for that startup only, without permanently changing the boot order.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 4 — Boot the Rocky Linux Installer
&lt;/h2&gt;

&lt;p&gt;With the BIOS configured, your laptop will boot into the Rocky Linux installer. You'll see a dark GRUB menu. Use the arrow keys to highlight &lt;strong&gt;"Install Rocky Linux"&lt;/strong&gt; and press Enter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The screen may go black for 30–60 seconds after you press Enter. The Linux kernel is loading and initializing your hardware. Do not panic and do not press any keys. Wait for the graphical installer (Anaconda) to appear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Select &lt;strong&gt;English (United States)&lt;/strong&gt; and click &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 5 — Anaconda Installer Walkthrough
&lt;/h2&gt;

&lt;p&gt;Anaconda uses a &lt;strong&gt;hub-and-spoke model&lt;/strong&gt;: one main summary screen with several sections listed. Complete them in any order. The &lt;strong&gt;"Begin Installation"&lt;/strong&gt; button activates only once every required section has a green checkmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time &amp;amp; Date
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Time &amp;amp; Date&lt;/strong&gt;. Click your region on the world map. Enable &lt;strong&gt;Network Time&lt;/strong&gt; if connected to Wi-Fi. Click &lt;strong&gt;Done&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software Selection
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Recommended for a DevOps workstation:&lt;/strong&gt; Choose &lt;strong&gt;"Workstation"&lt;/strong&gt; for the full GNOME desktop. For a leaner, server-like experience, choose &lt;strong&gt;"Server with GUI"&lt;/strong&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Under &lt;strong&gt;Additional Software&lt;/strong&gt;, also check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development Tools&lt;/strong&gt; — gcc, make, git, and other build essentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Tools&lt;/strong&gt; — useful system administration utilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless Management&lt;/strong&gt; — includes the SSH server for remote access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Management&lt;/strong&gt; — Podman and container tools (RHEL's Docker equivalent)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Done&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation Destination — Partitioning (Most Critical Step)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is the most important step in the entire guide.&lt;/strong&gt; Read every instruction carefully before clicking anything. An incorrect choice here can erase your Windows installation. There is no undo button once you accept changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click &lt;strong&gt;Installation Destination&lt;/strong&gt;. Select your internal drive. Under Storage Configuration, select &lt;strong&gt;Custom&lt;/strong&gt;. Click &lt;strong&gt;Done&lt;/strong&gt; to enter the manual partitioning screen.&lt;/p&gt;

&lt;h4&gt;
  
  
  Partitioning Scheme: Standard vs LVM
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Partition&lt;/td&gt;
&lt;td&gt;Simplicity — what you see is what you get&lt;/td&gt;
&lt;td&gt;Fine for beginners who want clarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LVM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flexibility — resize partitions later without data loss&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Recommended for DevOps&lt;/strong&gt; — standard in enterprise RHEL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is LVM?&lt;/strong&gt; LVM adds an abstraction layer between physical partitions and what the OS sees as drives. Instead of a fixed partition of exactly 40 GB, you have a "logical volume" that can be grown or shrunk while the system is running. In enterprise environments you'll constantly encounter LVM-managed disks. Choose LVM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Creating the Partitions
&lt;/h4&gt;

&lt;p&gt;Use the &lt;strong&gt;+&lt;/strong&gt; button to add each partition in this order:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never format the EFI partition.&lt;/strong&gt; It contains the Windows Boot Manager. Formatting it will make Windows unbootable. Set the mount point to &lt;code&gt;/boot/efi&lt;/code&gt; and ensure the &lt;strong&gt;format checkbox is unchecked&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Partition 1 — Reuse the EFI Partition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click on the existing EFI System Partition in the left panel, then in the right panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount Point: &lt;code&gt;/boot/efi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;File System: FAT32&lt;/li&gt;
&lt;li&gt;Ensure &lt;strong&gt;"Do not format"&lt;/strong&gt; is selected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Partition 2 — /boot&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount point: &lt;code&gt;/boot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Desired capacity: &lt;code&gt;1 GiB&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;File system: &lt;code&gt;ext4&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Why ext4 for /boot?&lt;/em&gt; GRUB reads &lt;code&gt;/boot&lt;/code&gt; at a very early stage when LVM may not yet be available. ext4 is simple, widely supported, and reliable in this context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition 3 — swap&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount point: &lt;code&gt;swap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Desired capacity: same as your RAM (e.g., &lt;code&gt;16 GiB&lt;/code&gt; for 16 GB of RAM)&lt;/li&gt;
&lt;li&gt;File system: &lt;code&gt;swap&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;What is swap?&lt;/em&gt; When physical RAM fills up, Linux moves less-used data to the swap partition. It's also required for hibernation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition 4 — / (Root)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount point: &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Desired capacity: &lt;code&gt;50 GiB&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;File system: &lt;code&gt;xfs&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Why XFS?&lt;/em&gt; XFS is the default file system for RHEL 7+ and Rocky Linux. It excels at large files, high I/O workloads, and parallel access — all common in server environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition 5 — /home&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount point: &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Desired capacity: leave blank (uses all remaining unallocated space)&lt;/li&gt;
&lt;li&gt;File system: &lt;code&gt;xfs&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Why a separate /home?&lt;/em&gt; If you ever reinstall Rocky Linux, your user data, configurations, and project files survive untouched. Simply reinstall, mount &lt;code&gt;/home&lt;/code&gt; without formatting it, and pick up where you left off.&lt;/p&gt;

&lt;h4&gt;
  
  
  Review &amp;amp; Accept Changes
&lt;/h4&gt;

&lt;p&gt;Click &lt;strong&gt;Done&lt;/strong&gt;. In the &lt;strong&gt;"Summary of Changes"&lt;/strong&gt; dialog, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The EFI partition is listed as &lt;strong&gt;"mount only"&lt;/strong&gt; (not format)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/boot&lt;/code&gt;, &lt;code&gt;swap&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, and &lt;code&gt;/home&lt;/code&gt; are listed as &lt;strong&gt;"format and mount"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Your Windows C: partition &lt;strong&gt;does not appear in this list at all&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If everything looks correct, click &lt;strong&gt;"Accept Changes"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network &amp;amp; Hostname
&lt;/h3&gt;

&lt;p&gt;Connect to Wi-Fi. Set your &lt;strong&gt;hostname&lt;/strong&gt;. Click &lt;strong&gt;Apply&lt;/strong&gt; then &lt;strong&gt;Done&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Account &amp;amp; User Creation
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is the root account?&lt;/strong&gt; In Linux, &lt;code&gt;root&lt;/code&gt; is the superuser with unlimited power over the entire system. In Rocky Linux, the root account is &lt;strong&gt;disabled by default&lt;/strong&gt; — a deliberate security choice that mirrors how enterprise RHEL systems are configured. Instead, you create a regular admin user that uses &lt;code&gt;sudo&lt;/code&gt; to gain root-level privileges when needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root Account:&lt;/strong&gt; Leave it &lt;strong&gt;disabled&lt;/strong&gt; (the default).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Creation:&lt;/strong&gt; Set a full name, a short lowercase username , and a strong password. &lt;strong&gt;Check "Make this user administrator"&lt;/strong&gt; — this adds your user to the &lt;code&gt;wheel&lt;/code&gt; group, granting sudo access.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Click &lt;strong&gt;"Begin Installation"&lt;/strong&gt;. The installer will format partitions, install the OS and all selected packages, and configure GRUB. This takes &lt;strong&gt;15–45 minutes&lt;/strong&gt;. When finished, click &lt;strong&gt;"Reboot System"&lt;/strong&gt; and remove the USB.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 6 — First Boot &amp;amp; the GRUB Menu
&lt;/h2&gt;

&lt;p&gt;After the reboot, you'll see the &lt;strong&gt;GRUB boot menu&lt;/strong&gt; with two entries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rocky Linux&lt;/strong&gt; (and older kernel entries)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows Boot Manager&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, GRUB auto-selects Rocky Linux after a 5-second countdown. Select &lt;strong&gt;Rocky Linux&lt;/strong&gt; and let it boot to the GNOME login screen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If GRUB doesn't appear and Windows boots directly:&lt;/strong&gt; The HP BIOS is still prioritising the Windows Boot Manager over GRUB. Enter BIOS (&lt;code&gt;F10&lt;/code&gt; on startup) and move "Rocky Linux" or "GRUB" above "Windows Boot Manager" in the boot order. See the Troubleshooting section for a permanent fix.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 7 — Post-Install DevOps Setup
&lt;/h2&gt;

&lt;p&gt;Open the &lt;strong&gt;Terminal&lt;/strong&gt; application (&lt;code&gt;Ctrl + Alt + T&lt;/code&gt;) and run the following in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7.1 — Full System Update
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;dnf&lt;/code&gt; is Rocky Linux's package manager:&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 &lt;/span&gt;dnf &lt;span class="nt"&gt;-y&lt;/span&gt; upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7.2 — Enable EPEL Repository
&lt;/h3&gt;

&lt;p&gt;EPEL (Extra Packages for Enterprise Linux) provides thousands of additional packages not in the base RHEL repos — like a major PPA trusted across the enterprise Linux world.&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 &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; epel-release
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7.3 — Understand SELinux (Don't Disable It)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not disable SELinux.&lt;/strong&gt; The most common beginner advice online is to set SELinux to permissive or disabled. This destroys the entire point of using a RHEL-based system. Every enterprise RHEL deployment runs SELinux in enforcing mode. Learning to work with it — not around it — is a core career skill.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sestatus                        &lt;span class="c"&gt;# view current SELinux status&lt;/span&gt;
getenforce                      &lt;span class="c"&gt;# should say "Enforcing"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-m&lt;/span&gt; avc &lt;span class="nt"&gt;-ts&lt;/span&gt; recent &lt;span class="c"&gt;# view recent SELinux denials&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sealert &lt;span class="nt"&gt;-a&lt;/span&gt; /var/log/audit/audit.log  &lt;span class="c"&gt;# human-readable SELinux explanations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7.4 — Understand firewalld
&lt;/h3&gt;

&lt;p&gt;Rocky Linux uses &lt;code&gt;firewalld&lt;/code&gt; instead of Ubuntu's &lt;code&gt;ufw&lt;/code&gt;. Same concept, different syntax:&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 &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--state&lt;/span&gt;                         &lt;span class="c"&gt;# check it's running&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;span class="c"&gt;# see current rules&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;http  &lt;span class="c"&gt;# allow HTTP traffic&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;                        &lt;span class="c"&gt;# apply changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;h3&gt;
  
  
  Problem: Windows Boots Directly (No GRUB Menu)
&lt;/h3&gt;

&lt;p&gt;The HP BIOS is loading the Windows Boot Manager before GRUB. Open &lt;strong&gt;Command Prompt as Administrator&lt;/strong&gt; on Windows and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bcdedit /set {bootmgr} path \EFI\rocky\grubx64.efi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, boot from the Rocky Linux USB → Troubleshooting → Rescue, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grub2-install /dev/nvme0n1          &lt;span class="c"&gt;# for NVMe drives&lt;/span&gt;
grub2-mkconfig &lt;span class="nt"&gt;-o&lt;/span&gt; /boot/grub2/grub.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Problem: Windows Missing from GRUB Menu
&lt;/h3&gt;

&lt;p&gt;GRUB didn't detect Windows. Install &lt;code&gt;os-prober&lt;/code&gt; and regenerate the GRUB config:&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 &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; os-prober
&lt;span class="nb"&gt;sudo &lt;/span&gt;os-prober                        &lt;span class="c"&gt;# should output a line mentioning Windows&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;grub2-mkconfig &lt;span class="nt"&gt;-o&lt;/span&gt; /boot/grub2/grub.cfg
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Problem: Wi-Fi Doesn't Work After Install
&lt;/h3&gt;

&lt;p&gt;Identify your network card first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lspci | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; network
lspci | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; wireless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Realtek cards:&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 &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; linux-firmware
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Intel cards, check:&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 &lt;/span&gt;nmcli device status     &lt;span class="c"&gt;# list all network devices&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nmcli radio wifi on     &lt;span class="c"&gt;# ensure Wi-Fi radio is enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Problem: Can't Shrink Windows Enough (Phase 1)
&lt;/h3&gt;

&lt;p&gt;Disable hibernation and the pagefile temporarily. Run in Administrator Command Prompt on Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powercfg /h off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then retry Disk Management → Shrink Volume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem: "No Space Left" Errors When Installing Packages
&lt;/h3&gt;

&lt;p&gt;Your root partition (&lt;code&gt;/&lt;/code&gt;) is full. Check usage:&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;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;          &lt;span class="c"&gt;# shows partition sizes and usage&lt;/span&gt;
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /&lt;span class="k"&gt;*&lt;/span&gt;      &lt;span class="c"&gt;# shows what's consuming space in root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you used LVM, you can extend the root logical volume using space from another volume — one of the main advantages of LVM over standard partitioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem: SELinux Blocking an Application
&lt;/h3&gt;

&lt;p&gt;Don't disable SELinux — diagnose 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="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-m&lt;/span&gt; avc &lt;span class="nt"&gt;-ts&lt;/span&gt; recent | audit2why          &lt;span class="c"&gt;# explains the denial in plain English&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-m&lt;/span&gt; avc &lt;span class="nt"&gt;-ts&lt;/span&gt; recent | audit2allow &lt;span class="nt"&gt;-M&lt;/span&gt; myfix  &lt;span class="c"&gt;# generates a policy fix&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;semodule &lt;span class="nt"&gt;-i&lt;/span&gt; myfix.pp                               &lt;span class="c"&gt;# applies the fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Congratulations, you have now completely dual-booted Windows 11 &amp;amp; Rocky Linux on the same physical hardware!&lt;/p&gt;

</description>
      <category>linux</category>
      <category>microsoft</category>
      <category>rockylinux</category>
      <category>dualboot</category>
    </item>
    <item>
      <title>Horizontal Scaling PostgreSQL with Citus: A Practical Deep Dive</title>
      <dc:creator>William Kwabena Akoto</dc:creator>
      <pubDate>Wed, 28 Jan 2026 20:45:01 +0000</pubDate>
      <link>https://forem.com/kobbyprincee/horizontal-scaling-postgresql-with-citus-a-practical-deep-dive-3kf7</link>
      <guid>https://forem.com/kobbyprincee/horizontal-scaling-postgresql-with-citus-a-practical-deep-dive-3kf7</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As databases grow beyond the capabilities of a single server, teams face a critical decision: scale vertically by adding more resources to one machine, or scale horizontally by distributing data across multiple servers. While vertical scaling eventually hits physical and economic limits, horizontal scaling offers virtually unlimited growth potential.&lt;/p&gt;

&lt;p&gt;In this hands-on guide, we'll explore Citus—an open-source extension that transforms PostgreSQL into a distributed database. We'll build a real Citus cluster from scratch, demonstrate data distribution, implement foreign key relationships across distributed tables, and understand the principles that make horizontal scaling work.&lt;/p&gt;

&lt;p&gt;By the end, you'll have practical experience with distributed databases and understand both their power and their limitations.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Horizontal Scaling?
&lt;/h2&gt;

&lt;p&gt;Imagine a pizza restaurant with one chef making all the pizzas. During rush hour, orders pile up because one person can only work so fast. This is &lt;strong&gt;vertical scaling&lt;/strong&gt;—you could train the chef to work faster (add more CPU), give them better tools (add more RAM), or expand their workspace (add more storage). But eventually, you hit limits.&lt;/p&gt;

&lt;p&gt;Now imagine hiring three chefs, each handling different orders. This is &lt;strong&gt;horizontal scaling&lt;/strong&gt;—you distribute the workload across multiple workers. The more customers you have, the more chefs you can add.&lt;/p&gt;

&lt;p&gt;In database terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vertical scaling&lt;/strong&gt;: Bigger server (more CPU, RAM, storage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal scaling&lt;/strong&gt;: More servers working together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Citus brings horizontal scaling to PostgreSQL by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Splitting your data across multiple worker nodes (sharding)&lt;/li&gt;
&lt;li&gt;Keeping related data together on the same node (co-location)&lt;/li&gt;
&lt;li&gt;Maintaining PostgreSQL's ACID guarantees and SQL compatibility&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Architecture: Understanding Citus Components
&lt;/h2&gt;

&lt;p&gt;A Citus cluster consists of at least three components:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Coordinator Node
&lt;/h3&gt;

&lt;p&gt;The coordinator is like a project manager—it doesn't do the heavy lifting but knows where everything is. When you connect to a Citus cluster, you connect to the coordinator. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes queries to the appropriate workers&lt;/li&gt;
&lt;li&gt;Combines results from multiple workers&lt;/li&gt;
&lt;li&gt;Manages distributed transactions&lt;/li&gt;
&lt;li&gt;Stores metadata about data distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Worker Nodes
&lt;/h3&gt;

&lt;p&gt;Workers are the muscle of your cluster—they store the actual data and execute queries. Each worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Holds a subset of your data (shards)&lt;/li&gt;
&lt;li&gt;Executes queries locally on its data&lt;/li&gt;
&lt;li&gt;Communicates with other workers when needed&lt;/li&gt;
&lt;li&gt;Functions as a full PostgreSQL instance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Shards
&lt;/h3&gt;

&lt;p&gt;Shards are pieces of your distributed tables. Think of them as mini-tables that together form your complete dataset. When you distribute a table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Citus creates 32 shards by default (configurable)&lt;/li&gt;
&lt;li&gt;Shards are distributed across workers&lt;/li&gt;
&lt;li&gt;Each row goes to exactly one shard based on a hash function&lt;/li&gt;
&lt;li&gt;Related data can be co-located on the same worker&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setting Up a Citus Cluster on Digital Ocean
&lt;/h2&gt;

&lt;p&gt;For this demonstration, we'll create a three-node cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;citus-01&lt;/strong&gt;: Coordinator node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;citus-02&lt;/strong&gt;: Worker 1 &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;citus-03&lt;/strong&gt;: Worker 2 &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Install PostgreSQL and Citus Extension
&lt;/h3&gt;

&lt;p&gt;On all three droplets:&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;# Add PostgreSQL repository&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postgresql-common
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh

&lt;span class="c"&gt;# Install PostgreSQL 16&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postgresql-16

&lt;span class="c"&gt;# Install Citus extension&lt;/span&gt;
curl https://install.citusdata.com/community/deb.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postgresql-16-citus-12.1

&lt;span class="c"&gt;# Configure PostgreSQL to load Citus&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"shared_preload_libraries = 'citus'"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/postgresql/16/main/postgresql.conf

&lt;span class="c"&gt;# Restart PostgreSQL&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Configure Network Access
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/postgresql/16/main/postgresql.conf&lt;/code&gt; on all nodes:&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;listen_addresses&lt;/span&gt; = &lt;span class="s1"&gt;'*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;/etc/postgresql/16/main/pg_hba.conf&lt;/code&gt; on all nodes to allow connections:&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;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;0&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;0&lt;/span&gt;/&lt;span class="m"&gt;0&lt;/span&gt;               &lt;span class="n"&gt;md5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart 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="nb"&gt;sudo &lt;/span&gt;systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Initialize the Coordinator
&lt;/h3&gt;

&lt;p&gt;On the coordinator node (citus-01):&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="c1"&gt;-- Connect as postgres user&lt;/span&gt;
&lt;span class="n"&gt;sudo&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="n"&gt;psql&lt;/span&gt;

&lt;span class="c1"&gt;-- Create the Citus extension&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;citus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add worker nodes to the cluster&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;citus_add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10.114.0.11'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;citus_add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10.114.0.12'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Verify worker registration&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_get_active_worker_nodes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both worker nodes listed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  node_name  | node_port 
-------------+-----------
 10.114.0.11 |      5432
 10.114.0.12 |      5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Initialize Workers
&lt;/h3&gt;

&lt;p&gt;On each worker node (citus-02 and citus-03):&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="n"&gt;sudo&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="n"&gt;psql&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;citus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Citus cluster is now operational!&lt;/p&gt;




&lt;h2&gt;
  
  
  Demonstration 1: Basic Data Distribution
&lt;/h2&gt;

&lt;p&gt;Let's create a simple distributed table and observe how data spreads across workers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Distributed Table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- On the coordinator&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;citus_demo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Distribute the table by 'id' column&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_distributed_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'citus_demo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;create_distributed_table&lt;/code&gt; function tells Citus to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create 32 shards (mini-tables) by default&lt;/li&gt;
&lt;li&gt;Distribute these shards across available workers&lt;/li&gt;
&lt;li&gt;Route future queries based on the distribution column (id)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Inserting Data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;citus_demo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'New York'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Chicago'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Charlie'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Seattle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'David'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Boston'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Emma'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Denver'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Frank'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Portland'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Grace'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Miami'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Henry'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Atlanta'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Iris'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Phoenix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Jack'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Dallas'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Examining Distribution
&lt;/h3&gt;

&lt;p&gt;Check how shards are distributed:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_shards&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'citus_demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (abbreviated):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; table_name | shardid |    shard_name     | nodename    | nodeport | shard_size 
------------+---------+-------------------+-------------+----------+------------
 citus_demo |  102488 | citus_demo_102488 | 10.114.0.11 |     5432 |      16384
 citus_demo |  102489 | citus_demo_102489 | 10.114.0.12 |     5432 |      16384
 citus_demo |  102490 | citus_demo_102490 | 10.114.0.11 |     5432 |       8192
 ... (32 shards total)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We see 32 shards split between two workers. Some shards have data (16 KB), others are empty (8 KB).&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Shard Assignment
&lt;/h3&gt;

&lt;p&gt;See which shard each record belongs to:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;get_shard_id_for_distribution_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'citus_demo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;shard_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_demo&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id | user_name |   city   | shard_id 
----+-----------+----------+----------
  1 | Alice     | New York |   102489
  2 | Bob       | Chicago  |   102512
  3 | Charlie   | Seattle  |   102503
  4 | David     | Boston   |   102496
  5 | Emma      | Denver   |   102494
  6 | Frank     | Portland |   102508
  7 | Grace     | Miami    |   102496
  8 | Henry     | Atlanta  |   102488
  9 | Iris      | Phoenix  |   102516
 10 | Jack      | Dallas   |   102492
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that David (id=4) and Grace (id=7) share shard 102496—this happens when their IDs hash to the same shard. With only 10 records across 32 shards, collisions and empty shards are normal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewing Data on Individual Workers
&lt;/h3&gt;

&lt;p&gt;Connect directly to Worker 1 (10.114.0.11):&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="c1"&gt;-- Query a specific shard&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_demo_102488&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id | user_name |  city   | age |         created_at         
----+-----------+---------+-----+----------------------------
  8 | Henry     | Atlanta |  31 | 2026-01-23 22:42:50.523619
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worker 1 only stores the data in its shards. Query other shards on this worker:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_demo_102512&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Bob&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_demo_102496&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- David and Grace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect to Worker 2 (10.114.0.12) and query:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_demo_102489&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Alice&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_demo_102503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- Charlie&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Each worker only stores a subset of the data. When you query the main table through the coordinator, Citus automatically fetches data from all workers and combines the results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demonstration 2: Foreign Keys and Co-location
&lt;/h2&gt;

&lt;p&gt;One of the biggest challenges in distributed databases is maintaining relationships between tables. Let's explore how Citus handles foreign keys through co-location.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge
&lt;/h3&gt;

&lt;p&gt;Imagine you have users and their orders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users table: Alice, Bob, Charlie&lt;/li&gt;
&lt;li&gt;Orders table: Order 1 (Alice's pizza), Order 2 (Bob's burger), Order 3 (Alice's soda)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a distributed setup, what if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alice's user record is on Worker 1&lt;/li&gt;
&lt;li&gt;Alice's orders are on Worker 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When inserting an order for Alice, Worker 2 must verify Alice exists—but Alice is on Worker 1! This requires expensive cross-node communication for every foreign key check.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Co-location
&lt;/h3&gt;

&lt;p&gt;Co-location ensures related data lives on the same worker. Citus achieves this by distributing both tables using the same column.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Co-located Tables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create users table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create orders table with composite primary key&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Must include distribution column&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Distribute both tables by user_id&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_distributed_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_distributed_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add foreign key constraint&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_user&lt;/span&gt; 
&lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: The orders primary key includes &lt;code&gt;user_id&lt;/code&gt; because Citus requires the distribution column in unique constraints. This ensures uniqueness can be verified locally on each worker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inserting Related Data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bob'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Charlie'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'David'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Emma'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Pizza'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Soda'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ice Cream'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Burger'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Fries'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Taco'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Burrito'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Nachos'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Pasta'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Salad'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verifying Co-location
&lt;/h3&gt;

&lt;p&gt;Check which shards contain user and order data:&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;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_shard_id_for_distribution_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;user_shard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_shard_id_for_distribution_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;order_shard&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; user_id |  name   |   item    | user_shard | order_shard 
---------+---------+-----------+------------+-------------
       1 | Alice   | Pizza     |     102521 |      102553
       1 | Alice   | Soda      |     102521 |      102553
       1 | Alice   | Ice Cream |     102521 |      102553
       2 | Bob     | Burger    |     102544 |      102576
       2 | Bob     | Fries     |     102544 |      102576
       3 | Charlie | Taco      |     102535 |      102567
       3 | Charlie | Burrito   |     102535 |      102567
       3 | Charlie | Nachos    |     102535 |      102567
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wait, the shard IDs are different!&lt;/strong&gt; This might seem wrong, but it's actually correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Different Shard IDs
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;users&lt;/code&gt; table has its own set of shards (102521, 102544, 102535...), and the &lt;code&gt;orders&lt;/code&gt; table has different shard IDs (102553, 102576, 102567...). However, what matters is that they're on the &lt;strong&gt;same physical worker node&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Verify this:&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;SELECT&lt;/span&gt; 
    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;shardid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nodename&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_shards&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;shardid&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;102521&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;102553&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Alice's user and order shards&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;nodename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; table_name | shardid |  nodename   
------------+---------+-------------
 orders     |  102553 | 10.114.0.11
 users      |  102521 | 10.114.0.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both shards are on Worker 1 (10.114.0.11)! This is co-location—different shard tables, same physical location.&lt;/p&gt;

&lt;p&gt;Think of it like two filing cabinets (users and orders) in the same office. Even though they're separate cabinets with different drawer labels, they're in the same room, so finding related information is instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Foreign Key Constraints
&lt;/h3&gt;

&lt;p&gt;Try inserting an order for a non-existent user:&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ghost Pizza'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  insert or update on table "orders" violates foreign key constraint "orders_user_id_fkey"
DETAIL:  Key (user_id)=(999) is not present in table "users".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect! The foreign key constraint works. Because user_id=999 doesn't exist, and thanks to co-location, the worker can check this locally without network calls to other workers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performing JOINs
&lt;/h3&gt;

&lt;p&gt;Count orders per user:&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;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;num_orders&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; user_id |  name   | num_orders 
---------+---------+------------
       1 | Alice   |          3
       2 | Bob     |          2
       3 | Charlie |          3
       4 | David   |          1
       5 | Emma    |          1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JOIN is fast because each worker can join its local user and order data without coordinating with other workers. This is the power of co-location.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Concepts and Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Distribution Column Selection
&lt;/h3&gt;

&lt;p&gt;Choose your distribution column carefully:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good choices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt; for multi-tenant applications (each tenant's data together)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;account_id&lt;/code&gt; for SaaS applications&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customer_id&lt;/code&gt; for e-commerce&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bad choices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; (causes temporal hotspots)&lt;/li&gt;
&lt;li&gt;Low-cardinality columns (poor distribution)&lt;/li&gt;
&lt;li&gt;Columns that change frequently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt;: Pick a column that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Appears in most queries (for query efficiency)&lt;/li&gt;
&lt;li&gt;Enables co-location of related tables&lt;/li&gt;
&lt;li&gt;Has high cardinality (many unique values)&lt;/li&gt;
&lt;li&gt;Remains stable (doesn't change)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Co-location Requirements
&lt;/h3&gt;

&lt;p&gt;For foreign keys to work across distributed tables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same distribution column&lt;/strong&gt;: Both parent and child tables must be distributed by the same column&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include in constraints&lt;/strong&gt;: The distribution column must be part of PRIMARY KEY and UNIQUE constraints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign key on distribution column&lt;/strong&gt;: The foreign key must reference the distribution column&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Shard Count
&lt;/h3&gt;

&lt;p&gt;Default: 32 shards per table&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too few shards&lt;/strong&gt;: Limits parallelism and future scalability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too many shards&lt;/strong&gt;: Increases metadata overhead and query planning time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most applications, 32-128 shards works well. Adjust based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expected data size&lt;/li&gt;
&lt;li&gt;Number of workers (more workers = more shards beneficial)&lt;/li&gt;
&lt;li&gt;Query patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. When to Use Citus
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Good use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-tenant SaaS applications&lt;/li&gt;
&lt;li&gt;Real-time analytics on time-series data&lt;/li&gt;
&lt;li&gt;High-throughput transactional workloads&lt;/li&gt;
&lt;li&gt;Applications that exceed single-server capacity&lt;/li&gt;
&lt;li&gt;Workloads with natural sharding keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not ideal for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small databases (&amp;lt; 100GB)&lt;/li&gt;
&lt;li&gt;Workloads requiring many cross-shard JOINs&lt;/li&gt;
&lt;li&gt;Applications with no clear distribution key&lt;/li&gt;
&lt;li&gt;Use cases where a single PostgreSQL instance suffices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Query Routing
&lt;/h3&gt;

&lt;p&gt;Citus routes queries differently based on their scope:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Single-shard queries&lt;/strong&gt; (fastest):&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="c1"&gt;-- Routed to one worker because user_id=1 uniquely determines shard&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Co-located JOINs&lt;/strong&gt; (fast):&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="c1"&gt;-- Each worker joins its local data, then results are combined&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Broadcast queries&lt;/strong&gt; (slower):&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="c1"&gt;-- Must query all shards and combine results&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Pizza'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Cross-shard queries&lt;/strong&gt; (slowest):&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="c1"&gt;-- Requires moving data between workers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Only works if products is a reference table or co-located&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reference Tables
&lt;/h3&gt;

&lt;p&gt;For small lookup tables (countries, products, categories), use reference tables:&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Replicate to all workers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_reference_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'countries'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference tables are fully replicated to every worker, enabling efficient JOINs with distributed tables.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring and Maintenance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Checking Cluster Health
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- View active workers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_get_active_worker_nodes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- Check shard distribution&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;nodename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;shard_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_shards&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;nodename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Monitor distributed queries&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_stat_statements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check replication lag (if using HA setup)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_replication_status&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rebalancing Shards
&lt;/h3&gt;

&lt;p&gt;As you add workers, rebalance shards:&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="c1"&gt;-- Move shards to balance load&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;citus_rebalance_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- Check rebalance progress&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;citus_rebalance_status&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;









&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We've successfully demonstrated horizontal scaling with Citus by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building a distributed cluster&lt;/strong&gt; with one coordinator and two workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creating distributed tables&lt;/strong&gt; and observing how data spreads across shards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementing foreign key relationships&lt;/strong&gt; through co-location&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifying co-location&lt;/strong&gt; by checking that related data resides on the same workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing foreign key constraints&lt;/strong&gt; to ensure data integrity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performing distributed JOINs&lt;/strong&gt; that execute efficiently due to co-location&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Citus transforms PostgreSQL into a horizontally scalable database while maintaining SQL compatibility and ACID guarantees. By distributing data intelligently and co-locating related tables, it achieves impressive performance even as datasets grow beyond single-server capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Elephant in the Room: Single Points of Failure
&lt;/h3&gt;

&lt;p&gt;However, our current setup has a critical limitation: &lt;strong&gt;every node is a single point of failure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider these scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coordinator failure&lt;/strong&gt;: If citus-01 goes down, your entire application stops. No queries can be routed, no data can be accessed, even though workers are healthy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker failure&lt;/strong&gt;: If citus-02 crashes, all data on that worker becomes unavailable. Half your users suddenly can't access their orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance downtime&lt;/strong&gt;: Need to apply a security patch? You'll have to take the coordinator offline, causing application downtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production environments, single points of failure are unacceptable. Your database must survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardware failures&lt;/li&gt;
&lt;li&gt;Network issues&lt;/li&gt;
&lt;li&gt;Planned maintenance&lt;/li&gt;
&lt;li&gt;Software crashes&lt;/li&gt;
&lt;li&gt;Data center outages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's Next: High Availability with Patroni
&lt;/h3&gt;

&lt;p&gt;In our next article, we'll solve these availability challenges by integrating &lt;strong&gt;Patroni&lt;/strong&gt;—an open-source high availability solution for PostgreSQL. We'll transform our vulnerable single-node cluster into a highly available system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture we'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coordinator cluster&lt;/strong&gt;: Primary + Standby (automatic failover)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker 1 cluster&lt;/strong&gt;: Primary + Standby
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker 2 cluster&lt;/strong&gt;: Primary + Standby&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;etcd cluster&lt;/strong&gt;: Distributed consensus for leader election&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you'll learn:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up Patroni with etcd for distributed consensus&lt;/li&gt;
&lt;li&gt;Configuring automatic failover for coordinators and workers&lt;/li&gt;
&lt;li&gt;Testing failure scenarios (simulating crashes)&lt;/li&gt;
&lt;li&gt;Monitoring cluster health and replication lag&lt;/li&gt;
&lt;li&gt;Performing switchovers for maintenance&lt;/li&gt;
&lt;li&gt;Understanding trade-offs between availability and consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining Citus's horizontal scalability with Patroni's high availability, you'll have a production-ready distributed database that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scale to billions of rows&lt;/li&gt;
&lt;li&gt;Handle millions of queries per second&lt;/li&gt;
&lt;li&gt;Survive node failures automatically&lt;/li&gt;
&lt;li&gt;Support zero-downtime maintenance&lt;/li&gt;
&lt;li&gt;Provide strong consistency guarantees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned for &lt;strong&gt;"High Availability for Citus: Implementing Automatic Failover with Patroni"&lt;/strong&gt; where we'll make this scalable database truly resilient.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Citus Documentation&lt;/strong&gt;: &lt;a href="https://docs.citusdata.com" rel="noopener noreferrer"&gt;https://docs.citusdata.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Citus GitHub&lt;/strong&gt;: &lt;a href="https://github.com/citusdata/citus" rel="noopener noreferrer"&gt;https://github.com/citusdata/citus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL Documentation&lt;/strong&gt;: &lt;a href="https://www.postgresql.org/docs/" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital Ocean Citus Tutorial&lt;/strong&gt;: &lt;a href="https://docs.digitalocean.com/products/databases/postgresql/" rel="noopener noreferrer"&gt;https://docs.digitalocean.com/products/databases/postgresql/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Special thanks to the Citus team for building such an elegant distributed database solution.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>distributedatabases</category>
      <category>citus</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Implementing Identity and Access Management (IAM) on Jenkins Using the Role-Based Strategy Plugin</title>
      <dc:creator>William Kwabena Akoto</dc:creator>
      <pubDate>Sat, 01 Nov 2025 14:21:39 +0000</pubDate>
      <link>https://forem.com/kobbyprincee/implementing-identity-and-access-management-iam-on-jenkins-using-the-role-based-strategy-plugin-51f6</link>
      <guid>https://forem.com/kobbyprincee/implementing-identity-and-access-management-iam-on-jenkins-using-the-role-based-strategy-plugin-51f6</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In modern DevOps environments, &lt;strong&gt;security and governance&lt;/strong&gt; are just as important as speed and automation. As organizations increasingly rely on Jenkins to manage continuous integration and delivery (CI/CD) pipelines, ensuring that users have the right level of access becomes crucial.&lt;/p&gt;

&lt;p&gt;Unrestricted or poorly managed access can lead to serious issues, from accidental job deletions and credential leaks to unauthorized configuration changes that impact production systems. To prevent this, Jenkins offers a powerful mechanism for implementing &lt;strong&gt;Identity and Access Management (IAM)&lt;/strong&gt; using the &lt;strong&gt;Role-Based Strategy Plugin&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article explains how to design and implement a robust IAM model in Jenkins using the &lt;strong&gt;Role-Based Strategy Plugin&lt;/strong&gt;, ensuring secure, efficient, and auditable control over who can view, build, or manage various Jenkins resources.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Identity and Access Management in Jenkins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Identity and Access Management (IAM)&lt;/strong&gt; in Jenkins revolves around defining &lt;em&gt;who&lt;/em&gt; the users are and &lt;em&gt;what&lt;/em&gt; actions they are allowed to perform. In practice, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity management&lt;/strong&gt; — creating and managing user accounts (locally or through external systems like LDAP or SSO).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access management&lt;/strong&gt; — granting specific permissions that define what each user can see or do within Jenkins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining these two concepts, administrators can enforce the &lt;strong&gt;principle of least privilege&lt;/strong&gt; — giving users only the permissions they need to perform their roles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use the Role-Based Strategy Plugin?
&lt;/h2&gt;

&lt;p&gt;Out of the box, Jenkins provides basic authorization modes such as “Matrix-based security” or “Logged-in users can do anything.” However, these options lack flexibility and scalability for organizations with multiple teams, roles, and environments.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Role-Based Strategy Plugin&lt;/strong&gt; enhances Jenkins’ native capabilities by allowing administrators to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define &lt;strong&gt;custom roles&lt;/strong&gt; (e.g., Administrator, Developer, DevOps).&lt;/li&gt;
&lt;li&gt;Assign &lt;strong&gt;granular permissions&lt;/strong&gt; at global, project, or agent levels.&lt;/li&gt;
&lt;li&gt;Manage access dynamically as teams grow or responsibilities shift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In essence, the plugin provides a &lt;strong&gt;structured IAM framework&lt;/strong&gt; that simplifies user management and strengthens Jenkins’ security posture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you begin, ensure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jenkins is installed and accessible.&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;Administrator privileges&lt;/strong&gt; on the Jenkins instance.&lt;/li&gt;
&lt;li&gt;Jenkins uses the &lt;strong&gt;“Jenkins’ own user database”&lt;/strong&gt; for managing local users (default setup).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Enable Local User Management
&lt;/h2&gt;

&lt;p&gt;To set up local identity management:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Manage Jenkins&lt;/strong&gt;, Click on &lt;strong&gt;Configure Global Security&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Security Realm&lt;/strong&gt;, select &lt;strong&gt;Jenkins’ own user database&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Allow users to sign up&lt;/strong&gt; if you want self-registration(Optional) &lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also create users manually via &lt;strong&gt;Manage Jenkins&lt;/strong&gt; ,Click on &lt;strong&gt;Manage Users&lt;/strong&gt; and then &lt;strong&gt;Create User&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This ensures that each identity is stored locally within Jenkins, forming the foundation for IAM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install the Role-Based Strategy Plugin
&lt;/h2&gt;

&lt;p&gt;To enable fine-grained access control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Manage Jenkins, Click on Plugins and Choose Available Plugins&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;Role-based Strategy&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select it and click &lt;strong&gt;Install without restart&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Restart Jenkins if prompted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once installed, this plugin allows you to define roles and assign permissions across the Jenkins environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enable Role-Based Authorization
&lt;/h2&gt;

&lt;p&gt;After installation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Manage Jenkins, Click on Configure Global Security&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Authorization&lt;/strong&gt;, select &lt;strong&gt;Role-Based Strategy&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your Jenkins instance will now use a role-based authorization model, allowing you to define and control permissions systematically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Define Roles
&lt;/h2&gt;

&lt;p&gt;Go to &lt;strong&gt;Manage Jenkins&lt;/strong&gt;, Click on &lt;strong&gt;Manage and Assign Roles&lt;/strong&gt; then choose &lt;strong&gt;Manage Roles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here, you can define three types of roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global roles&lt;/strong&gt; – apply across the entire Jenkins instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project roles&lt;/strong&gt; – apply to specific jobs or folders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent roles&lt;/strong&gt; – apply to particular build agents (nodes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we’ll focus on &lt;strong&gt;global roles&lt;/strong&gt; to define access boundaries for all users because they easy are to understand and use. Below are examples of roles to define and use:&lt;/p&gt;




&lt;h3&gt;
  
  
  Administrator Role
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Administrator&lt;/strong&gt; role represents users with complete control over the Jenkins environment.&lt;br&gt;
These users can manage all aspects of the system, including global configuration, plugin management, nodes, credentials, and user permissions.&lt;/p&gt;

&lt;p&gt;Typical permissions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall/Administer&lt;/strong&gt; — grants unrestricted access across Jenkins.&lt;/li&gt;
&lt;li&gt;Ability to install or remove plugins.&lt;/li&gt;
&lt;li&gt;Configuration of security, nodes, and system settings.&lt;/li&gt;
&lt;li&gt;Management of users, credentials, and roles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Administrators are responsible for maintaining system stability, enforcing security policies, and overseeing the overall Jenkins infrastructure.&lt;/p&gt;




&lt;h3&gt;
  
  
  Developer Role
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Developer&lt;/strong&gt; role is designed for users who actively build and test software projects but should not have access to system-level configurations.&lt;br&gt;
This role focuses on enabling developers to work efficiently within their pipelines while maintaining system security and consistency.&lt;/p&gt;

&lt;p&gt;Common permissions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall/Read&lt;/strong&gt; — allows visibility into Jenkins and its resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job/Build, Job/Read, Job/Discover&lt;/strong&gt; — enables users to trigger and monitor builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCM/Read, SCM/Tag&lt;/strong&gt; — provides access to integrated source control systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run/Delete, Run/Replay&lt;/strong&gt; — allows management of build executions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View/Read&lt;/strong&gt; — grants access to dashboards and views.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers can create, view, and execute jobs as part of their CI/CD process, but cannot alter Jenkins configurations or credentials.&lt;/p&gt;




&lt;h3&gt;
  
  
  DevOps Engineer Role
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;DevOps Engineer&lt;/strong&gt; role is intended for users who manage and optimize CI/CD workflows, pipelines, and build environments.&lt;br&gt;
This role strikes a balance between operational control and security, granting permissions to configure pipelines and credentials without granting full administrative rights.&lt;/p&gt;

&lt;p&gt;Typical permissions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overall/Read&lt;/strong&gt; — provides general visibility across Jenkins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials/Create, Update, Delete&lt;/strong&gt; — allows secure management of tokens, keys, and secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job/Build, Job/Create, Job/Configure, Job/Delete, Job/Read&lt;/strong&gt; — enables users to manage and maintain build jobs and pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCM/Read, SCM/Tag&lt;/strong&gt; — supports source control operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics/View&lt;/strong&gt; — provides access to monitoring and performance insights.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DevOps engineers are responsible for maintaining build automation, pipeline reliability, and integration with external tools,thus, ensuring continuous delivery processes run smoothly while adhering to security boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Assign Roles to Users
&lt;/h2&gt;

&lt;p&gt;Once roles are defined, assign them to specific users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Manage Jenkins, Click on Manage and Assign Roles then Assign Roles&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose the &lt;strong&gt;Global Roles&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Enter each username (exactly as created under “Manage Users”).&lt;/li&gt;
&lt;li&gt;Check the boxes corresponding to the roles you want to assign.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users now have access strictly within the limits of their assigned roles — enforcing identity-based access control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benefits of Role-Based IAM in Jenkins
&lt;/h2&gt;

&lt;p&gt;Implementing IAM using the Role-Based Strategy Plugin offers multiple benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Security&lt;/strong&gt; — Limits exposure by granting the least privileges necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational Governance&lt;/strong&gt; — Ensures every action is traceable to an authorized role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; — New users can be onboarded simply by assigning predefined roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accountability&lt;/strong&gt; — Clear role definitions make auditing and troubleshooting easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified Administration&lt;/strong&gt; — Reduces the complexity of managing large user bases.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating the &lt;strong&gt;Role-Based Strategy Plugin&lt;/strong&gt; with Jenkins’ built-in user management, administrators can achieve a strong, flexible, and scalable &lt;strong&gt;Identity and Access Management (IAM)&lt;/strong&gt; framework.&lt;/p&gt;

&lt;p&gt;This approach not only protects critical CI/CD operations but also promotes a culture of &lt;strong&gt;secure collaboration&lt;/strong&gt;. Each user operates within defined boundaries,thus,administrators maintain full control, developers focus on delivery, and DevOps engineers manage automation securely.&lt;/p&gt;

&lt;p&gt;In essence, implementing IAM on Jenkins using role-based authorization transforms a simple CI/CD tool into a &lt;strong&gt;secure, compliant, and enterprise-ready automation platform&lt;/strong&gt;. It enforces security best practices such as least privilege, transparency, and centralized control — key pillars of modern DevSecOps.&lt;/p&gt;




</description>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Install and Configure Apache Guacamole on a Linux Server</title>
      <dc:creator>William Kwabena Akoto</dc:creator>
      <pubDate>Sat, 18 Oct 2025 11:58:50 +0000</pubDate>
      <link>https://forem.com/kobbyprincee/how-to-install-and-configure-apache-guacamole-on-a-linux-server-2of</link>
      <guid>https://forem.com/kobbyprincee/how-to-install-and-configure-apache-guacamole-on-a-linux-server-2of</guid>
      <description>&lt;p&gt;This guide explains how to install and configure Apache Guacamole on a test server using &lt;strong&gt;Docker Compose&lt;/strong&gt;. The steps are simple and suitable for both beginners and experienced administrators.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What You Will Need&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Linux server&lt;/li&gt;
&lt;li&gt;Sudo or root privileges&lt;/li&gt;
&lt;li&gt;Docker and Docker Compose&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: Create the Guacamole Directory&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We’ll store all files related to Guacamole in one location. Create the following directories on your 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 mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /your-desired-folder/guacamole/data/mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final directory structure should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/your-desired-folder/guacamole
├── data
│   └── mysql/
├── docker-compose.yml
└── initdb.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;data&lt;/code&gt; directory will store Guacamole’s configuration and data.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;data/mysql&lt;/code&gt; directory will hold the MySQL database files.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;initdb.sql&lt;/code&gt; file will be used to initialize the database.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; file will define how all the services work together.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: Create the Docker Compose File&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Open a new file called &lt;code&gt;/your-desired-folder/guacamole/docker-compose.yml&lt;/code&gt; and add the following content:&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="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;guacamole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacamole/guacamole&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacamole&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;guacd&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-desired-port:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GUACD_HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacd&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacamole_db&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacuser&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacpass&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/your-desired-folder/guacamole/data:/app/data&lt;/span&gt;

  &lt;span class="na"&gt;guacd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacamole/guacd&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacd&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;

  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rootpass&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacamole_db&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacuser&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guacpass&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/your-desired-folder/guacamole/data/mysql:/var/lib/mysql&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/your-desired-folder/guacamole:/script&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file defines three main services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;guacamole&lt;/strong&gt; – The main web application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;guacd&lt;/strong&gt; – The Guacamole proxy daemon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mysql&lt;/strong&gt; – The database that stores user accounts, connection settings, and history.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 3: Understand the Directory Mappings&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each service in the Docker Compose file uses volumes to map directories from your host machine to the containers. This keeps your data safe even if containers are stopped or recreated.&lt;/p&gt;

&lt;p&gt;The first mapping connects the directory &lt;code&gt;/your-desired-folder/guacamole/data&lt;/code&gt; on the host to &lt;code&gt;/app/data&lt;/code&gt; inside the Guacamole container. This is where Guacamole stores its configuration files and runtime data. By keeping this data on the host, it remains safe even if the container is recreated or updated.&lt;/p&gt;

&lt;p&gt;The second mapping connects &lt;code&gt;/your-desired-folder/guacamole/data/mysql&lt;/code&gt; on the host to &lt;code&gt;/var/lib/mysql&lt;/code&gt; inside the MySQL container. This directory is where MySQL keeps all of its database files, such as user information, connection details, and history. Storing the database on the host ensures that all your Guacamole settings and accounts are preserved even after a restart.&lt;/p&gt;

&lt;p&gt;The third mapping links &lt;code&gt;/your-desired-folder/guacamole&lt;/code&gt; on the host to &lt;code&gt;/script&lt;/code&gt; inside the MySQL container. This is mainly used to share files between the host and the database container, especially during the initial setup. For example, you can place the &lt;code&gt;initdb.sql&lt;/code&gt; file here and use it to initialize the Guacamole database schema.&lt;/p&gt;

&lt;p&gt;Together, these mappings make the setup reliable and persistent, ensuring you don’t lose data or configuration when updating or restarting the containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How It Connects Visually&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host: /your-desired-folder/guacamole
│
├── data ---------------------&amp;gt; [guacamole:/app/data]
├── data/mysql ---------------&amp;gt; [mysql:/var/lib/mysql]
└── initdb.sql (via /script) -&amp;gt; [mysql:/script/initdb.sql]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;Step 4: Start the Containers &amp;amp; Initialize the Database&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now you can start Guacamole with Docker Compose:&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;cd&lt;/span&gt; /your-desired-folder/guacamole
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To confirm that all containers are running, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see three running containers: &lt;code&gt;guacamole&lt;/code&gt;, &lt;code&gt;guacd&lt;/code&gt;, and &lt;code&gt;mysql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run the following command to generate the schema file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; guacamole/guacamole /opt/guacamole/bin/initdb.sh &lt;span class="nt"&gt;--mysql&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /your-desired-folder/guacamole/initdb.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, import the schema into the MySQL container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;cp&lt;/span&gt; /your-desired-folder/guacamole/initdb.sql mysql:/initdb.sql
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; mysql bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"mysql -u root -p rootpass guacamole_db &amp;lt; /initdb.sql"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates all the necessary tables and relationships for Guacamole to work properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 5: Checking Logs and Troubleshooting&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If something doesn’t work as expected, you can view the container logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs guacamole
docker logs guacd
docker logs mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;Step 6: Access the Web Interface&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Open your web browser and go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://your-server-ip:8080/guacamole
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see the Guacamole login page.&lt;/p&gt;

&lt;p&gt;Default credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username: &lt;code&gt;guacadmin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: &lt;code&gt;guacadmin&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After logging in for the first time, you should change this password immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 7: Configure Remote Connections&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once you are logged in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Settings&lt;/strong&gt; in the top-right corner.&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Connections&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New Connection&lt;/strong&gt; to add an SSH, RDP, or VNC connection.&lt;/li&gt;
&lt;li&gt;Enter the details for the remote host you want to access.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guacamole will connect through your browser, and you’ll be able to control remote systems directly without extra software.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 9: Secure the Deployment&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For a test environment, running Guacamole on your desired port is fine.&lt;br&gt;
However, in production, it’s recommended to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up an &lt;strong&gt;Nginx reverse proxy&lt;/strong&gt; in front of Guacamole.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;HTTPS&lt;/strong&gt; using Let’s Encrypt or another SSL provider.&lt;/li&gt;
&lt;li&gt;Limit access through your firewall (allow only necessary ports).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures that your remote desktop connections remain encrypted and secure.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You’ve now set up and configured Apache Guacamole using Docker Compose. With this setup, you can easily access and manage remote servers through a browser without any additional client software.&lt;/p&gt;

&lt;p&gt;By following these steps, you have a clean, organized, and reproducible deployment that can easily be moved from a test to a production environment.&lt;/p&gt;




</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How To Deploy Uptime Kuma On A Linux Server With Docker Compose</title>
      <dc:creator>William Kwabena Akoto</dc:creator>
      <pubDate>Thu, 02 Oct 2025 18:27:43 +0000</pubDate>
      <link>https://forem.com/kobbyprincee/how-to-deploy-uptime-kuma-on-a-linux-server-with-docker-compose-500f</link>
      <guid>https://forem.com/kobbyprincee/how-to-deploy-uptime-kuma-on-a-linux-server-with-docker-compose-500f</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Uptime Kuma is an open-source, self-hosted monitoring tool. It helps you track the uptime, response time, and availability of your websites, APIs, servers, and other network resources.&lt;/p&gt;

&lt;p&gt;In this tutorial, I’ll walk through deploying &lt;strong&gt;Uptime Kuma&lt;/strong&gt; using &lt;strong&gt;Docker Compose&lt;/strong&gt; with persistent storage, making it easy to set up and manage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Linux server (Ubuntu, Debian, CentOS, etc.)&lt;/li&gt;
&lt;li&gt;Docker installed&lt;/li&gt;
&lt;li&gt;Docker Compose installed&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Directory Setup
&lt;/h2&gt;

&lt;p&gt;We’ll keep all Uptime Kuma files under &lt;code&gt;/your-desired-folder/uptime-kuma&lt;/code&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="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /your-desired-folder/uptime-kuma/data
&lt;span class="nb"&gt;cd&lt;/span&gt; /your-desired-folder/uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/your-desired-folder/
└── uptime-kuma/
    ├── data/                  
    └── docker-compose.yml    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Docker Compose File
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; inside &lt;code&gt;/your-desired-folder/uptime-kuma&lt;/code&gt;:&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="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;uptime-kuma&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;louislam/uptime-kuma&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uptime-kuma&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/your-desired-folder/uptime-kuma/data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-desired-port:3001"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Explanation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;image: louislam/uptime-kuma&lt;/strong&gt; → Uses the official Uptime Kuma Docker image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;container_name&lt;/strong&gt; → Assigns a fixed container name for easy management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;volumes&lt;/strong&gt; → Maps &lt;code&gt;/your-desired-folder/uptime-kuma/data&lt;/code&gt; (host) → &lt;code&gt;/app/data&lt;/code&gt; (container) for persistent storage.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ports: "your-desired-port:3001"&lt;/strong&gt; → Exposes Kuma’s web UI on port &lt;code&gt;your-desired-port&lt;/code&gt; of the host.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access via: &lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;:your-desired-port&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;restart: always&lt;/strong&gt; → Automatically restarts if the container stops or after a server reboot.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Deploy Uptime Kuma
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check container status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Accessing the Dashboard
&lt;/h2&gt;

&lt;p&gt;Once the container is running, open a browser and visit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;:your-desired-port
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first launch, you’ll be prompted to create an &lt;strong&gt;admin account&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Managing Uptime Kuma
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start service&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop service&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker-compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update to latest version&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker-compose pull
  docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use an &lt;strong&gt;NGINX reverse proxy&lt;/strong&gt; with &lt;strong&gt;Let’s Encrypt&lt;/strong&gt; for HTTPS instead of exposing Uptime Kuma directly on port &lt;code&gt;your-desired-port&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Regularly &lt;strong&gt;back up &lt;code&gt;/your-desired-folder/uptime-kuma/data&lt;/code&gt;&lt;/strong&gt; to avoid losing configs and monitoring history.&lt;/li&gt;
&lt;li&gt;Restrict access via &lt;strong&gt;firewall&lt;/strong&gt; if your server is exposed to the internet.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You now have &lt;strong&gt;Uptime Kuma running with Docker Compose&lt;/strong&gt;. With this setup, you can monitor your infrastructure and services easily, while keeping everything self-hosted and under your control.&lt;/p&gt;




</description>
      <category>docker</category>
      <category>monitoring</category>
      <category>tutorial</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
