<?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>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>
