<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mustafa ERBAY</title>
    <description>The latest articles on DEV Community by Mustafa ERBAY (@merbayerp).</description>
    <link>https://dev.to/merbayerp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3921203%2Fe3a198a1-49a0-466f-99e6-74bdf202a867.png</url>
      <title>DEV Community: Mustafa ERBAY</title>
      <link>https://dev.to/merbayerp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/merbayerp"/>
    <language>en</language>
    <item>
      <title>Declarative Server with NixOS: A Reproducible Infrastructure</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Tue, 18 Aug 2026 02:02:58 +0000</pubDate>
      <link>https://dev.to/merbayerp/declarative-server-with-nixos-a-reproducible-infrastructure-53ci</link>
      <guid>https://dev.to/merbayerp/declarative-server-with-nixos-a-reproducible-infrastructure-53ci</guid>
      <description>&lt;h2&gt;
  
  
  NixOS's Declarative Model
&lt;/h2&gt;

&lt;p&gt;NixOS is a Linux distribution that defines package management and system configuration within a single Nix expression. Packages defined in &lt;code&gt;nixpkgs&lt;/code&gt;, their dependency trees, and build options are described via &lt;strong&gt;Nix expressions&lt;/strong&gt;; these expressions are stored deterministically in the Nix store. System services, network settings, user accounts, and filesystem layout are declared in &lt;code&gt;configuration.nix&lt;/code&gt;; when the file changes, the &lt;code&gt;nixos-rebuild&lt;/code&gt; command can reevaluate the entire system.&lt;/p&gt;

&lt;p&gt;This approach embraces the principle that &lt;strong&gt;configuration is always a data source&lt;/strong&gt;. The same &lt;code&gt;configuration.nix&lt;/code&gt; produces identical results on two different machines, forming the core of the Infrastructure as Code (IaC) philosophy. NixOS also tracks the exact version and configuration of every package, thereby providing technical &lt;em&gt;reproducibility&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Basic Configuration File
&lt;/h2&gt;

&lt;p&gt;The following &lt;code&gt;configuration.nix&lt;/code&gt; example defines basic SSH access, an &lt;code&gt;nginx&lt;/code&gt; service, and a &lt;code&gt;postgresql&lt;/code&gt; database. After placing the file at &lt;code&gt;/etc/nixos/configuration.nix&lt;/code&gt;, run the &lt;code&gt;nixos-rebuild&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pkgs&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;nixpkgs/nixos/modules/installer/scan/not-detected.nix&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nv"&gt;boot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;grub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/dev/sda"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;networking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;hostName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"demo-server"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;openssh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nginx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;virtualHosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/var/www/example"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;postgresql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;postgresql_14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;dataDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/var/lib/postgresql/14/data"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;authentication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"securePassword"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;superuser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving this configuration file, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nixos-rebuild switch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command compiles the packages and brings the services up as defined. The output shows the &lt;em&gt;building&lt;/em&gt; and &lt;em&gt;activating&lt;/em&gt; phases; an example line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;building Nix store path... done
activating configuration... done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The &lt;code&gt;nixos-rebuild switch&lt;/code&gt; command modifies the current system configuration. In production environments, it is recommended to verify changes in a test environment before applying them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Managing Services: PostgreSQL and Nginx
&lt;/h2&gt;

&lt;p&gt;After the declarative definition, the actual state of the services is inspected with traditional &lt;code&gt;systemctl&lt;/code&gt; commands. The &lt;code&gt;systemctl&lt;/code&gt; output shows when the service was activated and the location of its unit file, allowing you to verify the effect of the Nix configuration on the system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status postgresql
systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example &lt;code&gt;postgresql&lt;/code&gt; status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;●&lt;/span&gt; &lt;span class="err"&gt;postgresql.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;PostgreSQL&lt;/span&gt; &lt;span class="err"&gt;RDBMS&lt;/span&gt;
   &lt;span class="err"&gt;Loaded:&lt;/span&gt; &lt;span class="err"&gt;loaded&lt;/span&gt; &lt;span class="err"&gt;(/etc/systemd/system/postgresql.service&lt;/span&gt;&lt;span class="c"&gt;; enabled)&lt;/span&gt;
   &lt;span class="err"&gt;Active:&lt;/span&gt; &lt;span class="err"&gt;active&lt;/span&gt; &lt;span class="err"&gt;(running)&lt;/span&gt; &lt;span class="err"&gt;since&lt;/span&gt; &lt;span class="err"&gt;Thu&lt;/span&gt; &lt;span class="err"&gt;2023-07-10&lt;/span&gt; &lt;span class="err"&gt;12:34:56&lt;/span&gt; &lt;span class="err"&gt;UTC&lt;/span&gt;&lt;span class="c"&gt;; 3min ago&lt;/span&gt;
   &lt;span class="err"&gt;Main&lt;/span&gt; &lt;span class="err"&gt;PID:&lt;/span&gt; &lt;span class="err"&gt;1342&lt;/span&gt; &lt;span class="err"&gt;(postgres)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A similar output is obtained for &lt;code&gt;nginx&lt;/code&gt;. This step demonstrates that the parameters defined in &lt;code&gt;configuration.nix&lt;/code&gt; are &lt;strong&gt;reflected on the actual system&lt;/strong&gt;. If a misconfiguration is discovered, simply correct the relevant section in &lt;code&gt;configuration.nix&lt;/code&gt; and rerun &lt;code&gt;nixos-rebuild switch&lt;/code&gt;; the system will automatically activate the new version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility and Disk Image
&lt;/h2&gt;

&lt;p&gt;To demonstrate reproducibility, copy the same &lt;code&gt;configuration.nix&lt;/code&gt; file to another machine and run the same &lt;code&gt;nixos-rebuild switch&lt;/code&gt; command. Additionally, NixOS’s &lt;code&gt;nixos-rebuild build-vm&lt;/code&gt; command converts the configuration into a QEMU image that can be tested in a CI environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nixos-rebuild build-vm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command produces a &lt;code&gt;disk.qcow2&lt;/code&gt; file under a &lt;code&gt;result&lt;/code&gt; directory. In a CI pipeline you can run this image as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test NixOS VM&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;qemu-system-x86_64 -drive file=result/disk.qcow2,format=qcow2,if=virtio \&lt;/span&gt;
      &lt;span class="s"&gt;-m 2048 -nographic -serial mon:stdio -snapshot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The &lt;code&gt;nixos-rebuild build-vm&lt;/code&gt; command creates a virtual‑machine image, and it is important to validate the image in a test environment before running it. Especially before moving to production, ensure the image starts the expected services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  NixOS Architecture and Package Management
&lt;/h2&gt;

&lt;p&gt;NixOS’s package management is built on a &lt;strong&gt;pure functional&lt;/strong&gt; model. Each package is produced by a Nix expression that defines only its inputs (source code, dependencies, build options). This approach guarantees the same output from the same inputs, so the same package on different machines shares the same hash. The &lt;code&gt;nix-env -iA&lt;/code&gt; command manages user‑level package installations, while &lt;code&gt;nixos-rebuild&lt;/code&gt; integrates system services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix-env &lt;span class="nt"&gt;-iA&lt;/span&gt; nixpkgs.htop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Package version certainty is achieved by pinning the &lt;code&gt;nixpkgs&lt;/code&gt; channel to a specific revision. For example, the following commands lock to a particular channel version; updating the channel is done with &lt;code&gt;nix-channel --update&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;nix-channel &lt;span class="nt"&gt;--add&lt;/span&gt; https://nixos.org/channels/nixos-23.11 nixos
nix-channel &lt;span class="nt"&gt;--update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Channel URLs and version numbers should be verified against the official NixOS documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Security and Updates
&lt;/h2&gt;

&lt;p&gt;NixOS applies security patches atomically to all packages in the system. The &lt;code&gt;nixos-rebuild switch --upgrade&lt;/code&gt; command pulls the latest packages from the defined channels and upgrades the system. This process requires no manual intervention via &lt;code&gt;systemctl&lt;/code&gt;; all services are restarted with the new packages.&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;nixos-rebuild switch &lt;span class="nt"&gt;--upgrade&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a security vulnerability is discovered, updating the &lt;code&gt;nixpkgs&lt;/code&gt; channel to the relevant revision is sufficient. Once the channel revision is updated, the system automatically adopts the new version.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Integration: GitHub Actions and NixOS
&lt;/h2&gt;

&lt;p&gt;Integrating declarative configuration into a CI/CD pipeline can be done by adding &lt;code&gt;nix&lt;/code&gt; commands directly to a GitHub Actions workflow. The example workflow below builds the &lt;code&gt;configuration.nix&lt;/code&gt; in a test VM, runs unit tests, and, if successful, stores the artifact.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NixOS CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Nix&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -L https://nixos.org/nix/install | sh&lt;/span&gt;
          &lt;span class="s"&gt;. $HOME/.nix-profile/etc/profile.d/nix.sh&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build VM&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nixos-rebuild build-vm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;qemu-system-x86_64 -drive file=result/disk.qcow2,format=qcow2,if=virtio \&lt;/span&gt;
            &lt;span class="s"&gt;-m 2048 -nographic -serial mon:stdio -snapshot -display none \&lt;/span&gt;
            &lt;span class="s"&gt;-monitor none -no-reboot -smp 2 -cpu host&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Artifact&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nixos-vm-image&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;result/disk.qcow2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow shows how a &lt;strong&gt;declarative&lt;/strong&gt; configuration can be automatically tested and stored as an artifact. A failed build in the CI process is quickly visible in the GitHub UI, preventing a faulty configuration from reaching production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback and Verification
&lt;/h2&gt;

&lt;p&gt;NixOS simplifies rolling back to a previous system snapshot. When a configuration error is detected, the following command can be used to revert to the last successful build:&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;nixos-rebuild switch &lt;span class="nt"&gt;--rollback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The &lt;code&gt;--rollback&lt;/code&gt; option reverts to the previous configuration on the current system. The behavior and availability of this command should be verified in the official documentation for the NixOS version you are using.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After a rollback, it is recommended to check the status of the services again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status nginx
systemctl status postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the rollback succeeds, the services should be in an &lt;code&gt;active (running)&lt;/code&gt; state. This mechanism relies on the principle of &lt;strong&gt;isolating process errors and re‑evaluating only the affected layer&lt;/strong&gt;, without needing to rebuild the entire system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Improvement Opportunities
&lt;/h2&gt;

&lt;p&gt;The strengths of the declarative approach include clear version compatibility and explicit package dependencies. However, some situations arise where the exact version you need is not available, or special build flags are missing. In such edge‑cases, you need to customize packages using &lt;code&gt;overrideAttrs&lt;/code&gt; or &lt;code&gt;packageOverrides&lt;/code&gt;, which can increase the complexity of the configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;pkgs&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;overlays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;super&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;myPython&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;python3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;overrideAttrs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"3.10.9"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;fetchFromGitHub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nv"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nv"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cpython"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nv"&gt;rev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"v3.10.9"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nv"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0v1c6w7zj..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;# Real hash&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another trade‑off is that the image‑building process &lt;strong&gt;consumes significant CPU and memory during the compilation phase&lt;/strong&gt;, which can strain resource planning on CI servers.&lt;/p&gt;

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

&lt;p&gt;The most robust approach to this topic is to treat claims and steps as small, verifiable pieces. Write down your assumptions and rollback plan before applying anything; then independently verify the expected outcome. When the environment, version, or conditions change, do not automatically assume the instructions still hold—re‑examine the relevant official sources. This way, decisions are based on an observable, repeatable process rather than a one‑time recipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mikeroyal/NixOS-Guide" rel="noopener noreferrer"&gt;NixOS Guide. Learn all about the immutable Nix Operating System ...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nixos.org/manual/nixos/stable/" rel="noopener noreferrer"&gt;NixOS Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.nixos.org/wiki/NixOS_Wiki" rel="noopener noreferrer"&gt;NixOS Wiki - Official NixOS Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nixos.org/manual/nixos/stable/options" rel="noopener noreferrer"&gt;Appendix A. Configuration Options - NixOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.nixos.org/wiki/Security" rel="noopener noreferrer"&gt;Security - Official NixOS Wiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>sistemmimarisi</category>
      <category>software</category>
    </item>
    <item>
      <title>Writing Idempotent Ansible Playbooks: Safe Change Patterns</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Mon, 17 Aug 2026 17:55:03 +0000</pubDate>
      <link>https://dev.to/merbayerp/writing-idempotent-ansible-playbooks-safe-change-patterns-34on</link>
      <guid>https://dev.to/merbayerp/writing-idempotent-ansible-playbooks-safe-change-patterns-34on</guid>
      <description>&lt;p&gt;A playbook returning &lt;code&gt;changed=0&lt;/code&gt; on its second run is a useful signal, but it is not proof of safe automation. A different input, target state, or external service can still produce a different outcome. A more precise goal is: &lt;strong&gt;with the same inputs and starting state, another run should create no new side effect; when a change is required, it should be verifiable and recoverable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;State-oriented modules such as &lt;code&gt;package&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;template&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, and &lt;code&gt;service&lt;/code&gt; make that goal easier. Free-form commands, external API calls, and custom &lt;code&gt;changed_when&lt;/code&gt; expressions put the burden of idempotence back on the playbook author.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describe State Before Procedures
&lt;/h2&gt;

&lt;p&gt;Write a task as “this state must exist,” not merely “run this command.” These tasks request the same user and directory state on every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure application group exists&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure application user exists&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;create_home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure configuration directory exists&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/myapp&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0750"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fully qualified collection names such as &lt;code&gt;ansible.builtin.file&lt;/code&gt; remove ambiguity when multiple collections expose the same short module name. Quoting numeric file modes also avoids YAML interpretation surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;--check&lt;/code&gt; Is a Prediction, Not Proof
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ansible-playbook site.yml --check --diff&lt;/code&gt; asks modules to predict changes without applying them. For modules that support it, &lt;code&gt;--diff&lt;/code&gt; also shows file differences. This is valuable during review, but it has two important limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modules do not all support check mode to the same extent.&lt;/li&gt;
&lt;li&gt;A command that is not executed cannot reveal its real return code or external effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not read check-mode output as a guarantee of production behavior. Disable diff output when rendered content contains secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Render application secrets&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets.conf.j2&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/myapp/secrets.conf&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0640"&lt;/span&gt;
  &lt;span class="na"&gt;no_log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;diff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stronger convergence test is to apply the playbook twice in a disposable environment. The first run may make required changes; the second should report no unexpected change.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;creates&lt;/code&gt;, &lt;code&gt;removes&lt;/code&gt;, and Honest &lt;code&gt;changed_when&lt;/code&gt; for Commands
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ansible.builtin.command&lt;/code&gt; cannot generally know whether the program it launches is idempotent. For operations represented by a filesystem artifact, &lt;code&gt;creates&lt;/code&gt; or &lt;code&gt;removes&lt;/code&gt; can prevent unnecessary execution and provide partial check-mode support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Inspect schema marker&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.stat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/myapp/.schema-v2&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema_v2&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run schema migration once&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/myapp/bin/migrate --to &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;creates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/myapp/.schema-v2&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;not schema_v2.stat.exists&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first task defines &lt;code&gt;schema_v2&lt;/code&gt; before the condition uses it. Trying to use a variable in a task's &lt;code&gt;when&lt;/code&gt; clause while creating that variable with the same task's &lt;code&gt;register&lt;/code&gt; is invalid: the result does not exist when the condition is evaluated.&lt;/p&gt;

&lt;p&gt;Mark a read-only probe as unchanged explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read current application version&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/myapp/bin/myapp --version&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp_version&lt;/span&gt;
  &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;failed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp_version.rc != &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;changed_when: false&lt;/code&gt; does not make a command safe. It only changes Ansible's report. If the command has a side effect, this expression merely hides the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate a File Before Replacing It
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;template&lt;/code&gt; module's &lt;code&gt;validate&lt;/code&gt; option runs a validation command against a temporary file before moving it to the destination. Ansible substitutes the temporary path for &lt;code&gt;%s&lt;/code&gt;; the command is not run through a shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Manage nginx configuration&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy nginx configuration after syntax validation&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx.conf.j2&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/nginx.conf&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0644"&lt;/span&gt;
        &lt;span class="na"&gt;backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/sbin/nginx -t -c %s&lt;/span&gt;
      &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload nginx&lt;/span&gt;

  &lt;span class="na"&gt;handlers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload nginx&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reloaded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file is unchanged, the task does not report a change and the handler is not notified. Even when several tasks notify the same handler, it normally runs once at the end of the relevant play section. That timing matters: if a later task fails, a notified handler may not run under the default behavior. Decide deliberately whether the failure model calls for &lt;code&gt;force_handlers&lt;/code&gt; or a controlled &lt;code&gt;meta: flush_handlers&lt;/code&gt;; both alter execution semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;block&lt;/code&gt; and &lt;code&gt;rescue&lt;/code&gt; Do Not Invent a Rollback
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rescue&lt;/code&gt; runs after a task in its block enters the failed state. It does not catch every failure class, such as an unreachable host or an invalid task definition. A rollback is also fictional if no restorable artifact was created first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy and verify nginx configuration&lt;/span&gt;
  &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install validated configuration&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx.conf.j2&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/nginx.conf&lt;/span&gt;
        &lt;span class="na"&gt;backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/sbin/nginx -t -c %s&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployed_config&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify installed configuration&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/sbin/nginx -t&lt;/span&gt;
      &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="na"&gt;rescue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore the backup created by template&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deployed_config.backup_file&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/nginx.conf&lt;/span&gt;
        &lt;span class="na"&gt;remote_src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployed_config.backup_file is defined&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify restored configuration&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/sbin/nginx -t&lt;/span&gt;
      &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a starting point, not a universal transaction. Service reload and traffic health checks still need environment-specific design. A rollback that has never been exercised is not reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate False Idempotence Signals
&lt;/h2&gt;

&lt;p&gt;Some playbooks look clean on a second run because they hide changes rather than prevent them. The common example is adding &lt;code&gt;changed_when: false&lt;/code&gt; to a command with side effects. A template that embeds the current time, a random value, or a newly generated token creates the opposite problem: the &lt;code&gt;template&lt;/code&gt; module correctly rewrites the file on every run. Fix the unstable input instead of masking the report.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;state: latest&lt;/code&gt; is not inherently wrong, but it follows a different contract from “same inputs, same result.” When a repository publishes a new package, the task intentionally changes the host. Keep such expected drift separate from convergence tests that require pinned versions, and document the distinction.&lt;/p&gt;

&lt;p&gt;For HTTP APIs, a filesystem &lt;code&gt;creates&lt;/code&gt; marker may be insufficient. Use an idempotency key when the API supports one. Otherwise, read the current resource, compare it with the desired representation, and write only when they differ. If a request times out after reaching the server, query the resource before blindly repeating a &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a remote marker or lock represents completion, create it atomically only after the operation and its verification succeed. A marker left by a failed operation can cause every later run to skip necessary recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the State After Applying It
&lt;/h2&gt;

&lt;p&gt;An idempotent task result is not the same as a healthy service. A configuration file can be correct while the process cannot reach a new dependency. Add a read-only health check after the change and bound the startup window with explicit retries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for local health endpoint&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:8080/health&lt;/span&gt;
    &lt;span class="na"&gt;status_code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
    &lt;span class="na"&gt;return_content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;health&lt;/span&gt;
  &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
  &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;health.status == &lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retries must not become an infinite wait. When health verification fails, the playbook or its calling deployment layer should state which backup is restored, how the service is reloaded, and how the restored state is verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Convergence in CI
&lt;/h2&gt;

&lt;p&gt;A safe pipeline should apply at least this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;ansible-playbook --syntax-check&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ansible-lint&lt;/code&gt; to inspect risky patterns and module usage.&lt;/li&gt;
&lt;li&gt;Apply the playbook to an isolated test host.&lt;/li&gt;
&lt;li&gt;Apply the same playbook again with the same inputs.&lt;/li&gt;
&lt;li&gt;Fail on every &lt;code&gt;failed&lt;/code&gt; result and every unexpected second-run &lt;code&gt;changed&lt;/code&gt; result.&lt;/li&gt;
&lt;li&gt;Keep intentional probes separate with an honest &lt;code&gt;changed_when: false&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A successful first run is not enough. Evaluate second-run convergence, handler counts, rendered-file differences, and service health together.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does This Decision Become Invalid?
&lt;/h2&gt;

&lt;p&gt;Re-test an idempotence claim when the module version, target package manager, external API behavior, template inputs, or service assumptions change. A &lt;code&gt;latest&lt;/code&gt; package state, a time-dependent API, or a template that generates random values can make the same playbook produce a later change.&lt;/p&gt;

&lt;p&gt;The objective is not to force every task to display &lt;code&gt;changed=0&lt;/code&gt;. The objective is automation that &lt;strong&gt;reports real changes honestly, validates before replacement, measures after application, and follows a rollback path that has actually been tested&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_checkmode.html" rel="noopener noreferrer"&gt;Ansible — Validating tasks with check mode and diff mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/command_module.html" rel="noopener noreferrer"&gt;Ansible — &lt;code&gt;ansible.builtin.command&lt;/code&gt; module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_handlers.html" rel="noopener noreferrer"&gt;Ansible — Handler execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html" rel="noopener noreferrer"&gt;Ansible — Blocks and error handling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html" rel="noopener noreferrer"&gt;Ansible — &lt;code&gt;ansible.builtin.template&lt;/code&gt; and &lt;code&gt;validate&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/command.py" rel="noopener noreferrer"&gt;GitHub — Source of &lt;code&gt;ansible.builtin.command&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>guide</category>
      <category>software</category>
    </item>
    <item>
      <title>Log Infrastructure with Loki: Label Design and Cost Control</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Mon, 17 Aug 2026 08:03:10 +0000</pubDate>
      <link>https://dev.to/merbayerp/log-infrastructure-with-loki-label-design-and-cost-control-j9i</link>
      <guid>https://dev.to/merbayerp/log-infrastructure-with-loki-label-design-and-cost-control-j9i</guid>
      <description>&lt;p&gt;Loki deployments often become expensive in the label model before they run out of storage. Turning every &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;request_id&lt;/code&gt;, pod name, or file path into an indexed label does more than make a field searchable: it creates more log streams, encourages smaller chunks, and expands the index. Cost control is therefore not a compression setting to add later. It is a schema decision that belongs before ingestion starts.&lt;/p&gt;

&lt;p&gt;This guide does not offer a magic &lt;code&gt;config.yaml&lt;/code&gt; to paste into every deployment. It shows how to classify fields, where to place high-cardinality metadata, how to enable retention safely, and how to verify that a change reduced operational cost instead of merely moving it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Loki costs originate
&lt;/h2&gt;

&lt;p&gt;Rather than indexing every word in each log line as a conventional full-text engine would, Loki organizes log streams by label sets. Entries with the same label names and values belong to the same stream. Changing one value creates a different stream. A field such as &lt;code&gt;environment="prod"&lt;/code&gt;, with a small and stable value set, fits this model. A &lt;code&gt;request_id&lt;/code&gt; that changes for every request does not.&lt;/p&gt;

&lt;p&gt;High cardinality creates cost in several places. First, ingesters must track more active streams. Second, sparse streams can close chunks before those chunks become usefully large, increasing the number of small objects. Third, the index and query planner must deal with a wider stream set. The impact is not limited to disk usage; memory, object-store requests, and query latency can all increase.&lt;/p&gt;

&lt;p&gt;The useful question is not simply, "Do people search by this field?" Ask instead: "Is this field stable and low-cardinality enough to narrow stream selection effectively?" Frequent searches for a high-cardinality field do not automatically make it a good indexed label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexed label, structured metadata, or log body?
&lt;/h2&gt;

&lt;p&gt;The following policy is a practical starting point. Validate the final decision against your real traffic and query patterns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field example&lt;/th&gt;
&lt;th&gt;Recommended location&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;environment&lt;/code&gt;, &lt;code&gt;cluster&lt;/code&gt;, &lt;code&gt;namespace&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Indexed label&lt;/td&gt;
&lt;td&gt;Small, relatively stable value set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;service_name&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Indexed label&lt;/td&gt;
&lt;td&gt;Usually the first selector in a query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;level&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Measured choice: label or parsed field&lt;/td&gt;
&lt;td&gt;Low value count, but not always needed on every stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pod&lt;/code&gt;, &lt;code&gt;container_id&lt;/code&gt;, &lt;code&gt;process_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Structured metadata&lt;/td&gt;
&lt;td&gt;Operationally useful but highly cardinal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;trace_id&lt;/code&gt;, &lt;code&gt;request_id&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Structured metadata or log body&lt;/td&gt;
&lt;td&gt;Can change per request and explode stream count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message, stack trace, free text&lt;/td&gt;
&lt;td&gt;Log body&lt;/td&gt;
&lt;td&gt;Evaluate through filtering and parsing after stream selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Current Grafana Loki documentation recommends moving fields such as pod names and &lt;code&gt;service.instance.id&lt;/code&gt; from index labels to structured metadata. Structured metadata attaches a field to an entry without placing it in the indexed label set or embedding it in the message. The feature requires schema &lt;code&gt;v13&lt;/code&gt; or newer and therefore chunk format V4. OTLP ingestion also relies on structured metadata, so do not disable it before checking your schema and ingestion path.&lt;/p&gt;

&lt;p&gt;A query makes the separation concrete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{service_name="checkout", environment="prod"}
  |= "timeout"
  | json
  | trace_id="0242ac120002"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The low-cardinality labels on the first line reduce the stream set. The &lt;code&gt;trace_id&lt;/code&gt; filter is evaluated over the selected entries. You can still find a single request without creating a separate indexed stream for every request identifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the label budget as a contract
&lt;/h2&gt;

&lt;p&gt;Do not let application teams add indexed labels as an unrestricted list. Every proposed label should have an owner, an expected number of unique values, a lifetime, and at least one example query. "It is small today" is not enough; document how the value set grows with tenants, customers, processes, or pods.&lt;/p&gt;

&lt;p&gt;A useful admission review asks four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the value set grow over hours, days, or months?&lt;/li&gt;
&lt;li&gt;Can &lt;code&gt;service_name&lt;/code&gt;, environment, and cluster already select the required streams?&lt;/li&gt;
&lt;li&gt;Can the same information live in structured metadata or the JSON body?&lt;/li&gt;
&lt;li&gt;Which measurements will prove that the change improved stream and chunk behavior?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Loki's &lt;code&gt;limits_config&lt;/code&gt; provides stream and query controls, but copying a universal number from a blog post is unsafe. Measure current p95 and p99 behavior, add headroom for expected growth, and introduce limits per tenant in stages. A limit does not repair a poor schema. It limits how much of the cluster that schema can consume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design storage schema and retention together
&lt;/h2&gt;

&lt;p&gt;For new installations, the current Loki documentation recommends &lt;code&gt;store: tsdb&lt;/code&gt; with &lt;code&gt;schema: v13&lt;/code&gt;. Object-storage cost is not only about how many days of logs you retain; schema choices also affect how index and chunk objects are organized. When changing the schema of an existing system, preserve the old period and add a new &lt;code&gt;period_config&lt;/code&gt; entry with an appropriate start date. Do not reinterpret existing data as if it had been written under the new schema.&lt;/p&gt;

&lt;p&gt;The following is deliberately a partial configuration. It shows the relationships that matter for retention, not a complete production deployment:&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;schema_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-08-01&lt;/span&gt;
      &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tsdb&lt;/span&gt;
      &lt;span class="na"&gt;object_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v13&lt;/span&gt;
      &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index_&lt;/span&gt;
        &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;

&lt;span class="na"&gt;limits_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;retention_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;720h&lt;/span&gt; &lt;span class="c1"&gt;# 30 days; derive this from business and legal needs&lt;/span&gt;

&lt;span class="na"&gt;compactor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;working_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/loki/compactor&lt;/span&gt;
  &lt;span class="na"&gt;retention_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;delete_request_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bucket name, authentication method, and &lt;code&gt;storage_config&lt;/code&gt; are intentionally omitted. Complete them from the current documentation for your Loki version and S3-compatible service. For a schema transition, choose the &lt;code&gt;from&lt;/code&gt; date as part of the rollout plan. For a new installation with no existing data, use a valid date in the past as described by the schema documentation.&lt;/p&gt;

&lt;p&gt;With TSDB or BoltDB Shipper, the Compactor applies retention. Setting &lt;code&gt;retention_period&lt;/code&gt; without enabling retention on the Compactor does not produce the expected deletion behavior. The documentation also requires a 24-hour index period and a configured &lt;code&gt;delete_request_store&lt;/code&gt; when retention is enabled. If you add an object-store lifecycle rule, keep it longer than Loki's retention period; deleting chunks first can leave index references to missing data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rollback boundary:&lt;/strong&gt; retention deletion is irreversible. Validate it in a test tenant first, check object-store versioning or backup policy, and monitor the Compactor's deletion work. Reverting the configuration can stop future deletion. It cannot reconstruct chunks that have already been removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bound query cost as well
&lt;/h2&gt;

&lt;p&gt;A sound label schema does not make every query safe. Wide time ranges, weak selectors, expensive regular expressions, and excessive parallelism can cause large object-store reads. Start dashboard queries with low-cardinality selectors, restrict the time range to the operational need, and apply JSON or regex parsing only after stream selection.&lt;/p&gt;

&lt;p&gt;If a dashboard or automation can starve other users inside a shared tenant, evaluate query fairness. Loki's query scheduler can use the &lt;code&gt;X-Loki-Actor-Path&lt;/code&gt; header to place actors into separate subqueues within a tenant. Generate that header in a controlled Grafana data source or authentication proxy instead of trusting arbitrary end-user input. Confirm the scheduler topology and supported configuration in the documentation for the version you operate before enabling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe rollout: measure, constrain, then move
&lt;/h2&gt;

&lt;p&gt;Apply a label redesign as a controlled migration rather than a cluster-wide edit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calculate the unique-value trend for each label over the last 24 hours and seven days.&lt;/li&gt;
&lt;li&gt;Identify the fastest-growing fields, especially request identities and ephemeral infrastructure identifiers.&lt;/li&gt;
&lt;li&gt;Move those fields from indexed labels to structured metadata or the log body in the collector configuration.&lt;/li&gt;
&lt;li&gt;Release the change to one service or tenant as a canary.&lt;/li&gt;
&lt;li&gt;Compare active streams, chunk creation, ingestion rejections, object-store requests, and query latency with the previous period.&lt;/li&gt;
&lt;li&gt;Update saved queries and dashboards for the new field location.&lt;/li&gt;
&lt;li&gt;Roll back the collector change if expected searches break; keep retention changes in a separate rollout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Grafana's current documentation recommends Grafana Alloy as the primary way to send logs to Loki, and Alloy can perform label and structured-metadata transformations in the ingestion pipeline. For older Promtail-based installations, check the current support timeline and map pipeline stages to Alloy components in a separate migration plan instead of combining collector migration with label and retention changes.&lt;/p&gt;

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

&lt;p&gt;The main control on Loki cost is not the compression level; it is the label schema that determines stream count. Keep stable, low-cardinality fields that genuinely reduce the search space as indexed labels. Move request, user, and ephemeral infrastructure identifiers to structured metadata or the log body. Treat TSDB v13, Compactor retention, and query limits as parts of the same design. Most importantly, do not call a rollout successful merely because ingestion still works. Require active-stream behavior, chunk efficiency, object-store traffic, and query latency to improve together.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/get-started/labels/" rel="noopener noreferrer"&gt;Grafana Loki: Understand labels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/get-started/labels/cardinality/" rel="noopener noreferrer"&gt;Grafana Loki: Cardinality&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/get-started/labels/structured-metadata/" rel="noopener noreferrer"&gt;Grafana Loki: Structured metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/operations/storage/schema/" rel="noopener noreferrer"&gt;Grafana Loki: Storage schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/operations/storage/retention/" rel="noopener noreferrer"&gt;Grafana Loki: Log retention&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/send-data/alloy/" rel="noopener noreferrer"&gt;Grafana Loki: Ingest logs with Alloy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/operations/query-fairness/" rel="noopener noreferrer"&gt;Grafana Loki: Query fairness within tenants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/grafana/loki" rel="noopener noreferrer"&gt;Grafana Loki source repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>loki</category>
      <category>logging</category>
      <category>observability</category>
      <category>grafana</category>
    </item>
    <item>
      <title>Prometheus Alert Fatigue: Symptom-Based Alert Design</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:57:50 +0000</pubDate>
      <link>https://dev.to/merbayerp/prometheus-alert-fatigue-symptom-based-alert-design-3cgk</link>
      <guid>https://dev.to/merbayerp/prometheus-alert-fatigue-symptom-based-alert-design-3cgk</guid>
      <description>&lt;p&gt;In a monitoring infrastructure that generates hundreds of alerts a day, the moment engineers stop reading notifications and start muting them, system observability is completely lost. Alert fatigue is the direct consequence of writing a separate alert rule for every possible anomaly in the system. The solution is a symptom-based alert architecture that focuses not on "Why did CPU hit 90%?", but on "Can users actually get a response from the service right now?"&lt;/p&gt;

&lt;p&gt;Symptom-based alert design makes the metrics measuring user-facing outages, latency, and error rates the primary notification triggers, rather than tracking the isolated states of subcomponents. Root causes are not used to fire alerts; instead, they serve as diagnostic dashboard metrics and log correlations once a symptom alert is triggered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJVc2VyIFJlcXVlc3QiXSAtLT4gQlsiU2VydmljZSAvIEdhdGV3YXkiXTsKICAgIEIgLS0-IENbIlBvc3RncmVTUUwiXTsKICAgIEIgLS0-IERbIlJlZGlzIENhY2hlIl07CiAgICBCIC0tPiBFWyJUaGlyZC1QYXJ0eSBBUEkiXTsKICAgIAogICAgc3ViZ3JhcGggIkNhdXNlLUJhc2VkIC0gVGhlIFdyb25nIEFwcHJvYWNoIgogICAgICAgIEMgLS4tPnwiSGlnaCBDUFUgQWxlcnQgKFBhZ2UpInwgRlsiT24tQ2FsbCBFbmdpbmVlciJdCiAgICAgICAgRCAtLi0-fCJDb25uZWN0aW9uIFNwaWtlIChQYWdlKSJ8IEYKICAgICAgICBFIC0uLT58IlRpbWVvdXQgU3Bpa2UgKFBhZ2UpInwgRgogICAgZW5kCiAgICAKICAgIHN1YmdyYXBoICJTeW1wdG9tLUJhc2VkIC0gVGhlIFJpZ2h0IEFwcHJvYWNoIgogICAgICAgIEIgPT0-fCJFcnJvciBSYXRlID4gMSUgb3IgcDk5IExhdGVuY3kgKFBhZ2UpInwgRgogICAgICAgIEMgLS4tPnwiRGFzaGJvYXJkIE1ldHJpYyAoRGlhZ25vc2lzKSJ8IEdbIkdyYWZhbmEgLyBSdW5ib29rIl0KICAgICAgICBEIC0uLT58IkRhc2hib2FyZCBNZXRyaWMgKERpYWdub3NpcykifCBHCiAgICAgICAgRSAtLi0-fCJEYXNoYm9hcmQgTWV0cmljIChEaWFnbm9zaXMpInwgRwogICAgZW5k%3Ftype%3Dpng%26bgColor%3Dwhite" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJVc2VyIFJlcXVlc3QiXSAtLT4gQlsiU2VydmljZSAvIEdhdGV3YXkiXTsKICAgIEIgLS0-IENbIlBvc3RncmVTUUwiXTsKICAgIEIgLS0-IERbIlJlZGlzIENhY2hlIl07CiAgICBCIC0tPiBFWyJUaGlyZC1QYXJ0eSBBUEkiXTsKICAgIAogICAgc3ViZ3JhcGggIkNhdXNlLUJhc2VkIC0gVGhlIFdyb25nIEFwcHJvYWNoIgogICAgICAgIEMgLS4tPnwiSGlnaCBDUFUgQWxlcnQgKFBhZ2UpInwgRlsiT24tQ2FsbCBFbmdpbmVlciJdCiAgICAgICAgRCAtLi0-fCJDb25uZWN0aW9uIFNwaWtlIChQYWdlKSJ8IEYKICAgICAgICBFIC0uLT58IlRpbWVvdXQgU3Bpa2UgKFBhZ2UpInwgRgogICAgZW5kCiAgICAKICAgIHN1YmdyYXBoICJTeW1wdG9tLUJhc2VkIC0gVGhlIFJpZ2h0IEFwcHJvYWNoIgogICAgICAgIEIgPT0-fCJFcnJvciBSYXRlID4gMSUgb3IgcDk5IExhdGVuY3kgKFBhZ2UpInwgRgogICAgICAgIEMgLS4tPnwiRGFzaGJvYXJkIE1ldHJpYyAoRGlhZ25vc2lzKSJ8IEdbIkdyYWZhbmEgLyBSdW5ib29rIl0KICAgICAgICBEIC0uLT58IkRhc2hib2FyZCBNZXRyaWMgKERpYWdub3NpcykifCBHCiAgICAgICAgRSAtLi0-fCJEYXNoYm9hcmQgTWV0cmljIChEaWFnbm9zaXMpInwgRwogICAgZW5k%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Diagram" width="1578" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do Cause-Based Alerts Fail?
&lt;/h2&gt;

&lt;p&gt;A cause-based alerting strategy stems from the engineering reflex of "writing a rule for every part that could possibly break." Waking up the on-call engineer whenever disk usage reaches 80% on a server, CPU consumption exceeds a specific threshold, or Redis memory usage climbs is a textbook example of this mindset. However, high CPU usage on a server does not necessarily mean the service provided by that server is degraded; often, it simply indicates that the system is utilizing its hardware resources efficiently.&lt;/p&gt;

&lt;p&gt;The fundamental problem created by this approach is that most alerts are non-actionable. When an on-call engineer gets paged at midnight for "Server CPU 85%", checks the system, and sees that users are experiencing zero errors and normal latency, they will completely tune out that alert channel within two weeks. Consequently, when a real database deadlock or network outage occurs, the critical alert gets lost in the noise.&lt;/p&gt;

&lt;p&gt;The table below summarizes the operational differences between both approaches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Cause-Based&lt;/th&gt;
&lt;th&gt;Symptom-Based&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trigger Focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPU, Memory, Disk, Thread count&lt;/td&gt;
&lt;td&gt;HTTP 5xx rate, p99 Latency, Queue backlog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alert Volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (Every component screams individually)&lt;/td&gt;
&lt;td&gt;Low (Fires only when users are impacted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Actionability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ambiguous (Often requires no action)&lt;/td&gt;
&lt;td&gt;Clear (Users cannot use the service)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance Burden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Thresholds must be constantly retuned&lt;/td&gt;
&lt;td&gt;Tied to SLO/SLI targets, stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited to known failure modes&lt;/td&gt;
&lt;td&gt;Catches unknown/novel failures as well&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Core Principles of Symptom-Based Alerting: RED and USE
&lt;/h2&gt;

&lt;p&gt;When setting up symptom-based monitoring in a distributed system, two foundational methodologies serve as your guide: &lt;strong&gt;RED&lt;/strong&gt; (Rate, Errors, Duration) for request-driven services, and &lt;strong&gt;USE&lt;/strong&gt; (Utilization, Saturation, Errors) for resource-driven infrastructure components. Applying these two approaches to the appropriate architectural layers cuts out unnecessary noise at the source.&lt;/p&gt;

&lt;p&gt;The RED method should be applied to layers directly exposing APIs to users or other services. A service's request rate per second (Rate), how many of those requests fail (Errors), and how long requests take to complete (Duration) directly reflect its health. Regardless of the memory consumption of the container behind the service, if the 5xx error rate exceeds the defined Service Level Objective (SLO) boundary, you have a real symptom that must be addressed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       +-------------------------------------------------------+
       |                  RED Method (Services)                |
       |  - Rate (RPS)                                         |
       |  - Errors (HTTP 5xx / gRPC Errors) -&amp;gt; ALERT           |
       |  - Duration (p95/p99 Latency)      -&amp;gt; ALERT           |
       +---------------------------+---------------------------+
                                   |
                                   v
       +-------------------------------------------------------+
       |             USE Method (Infrastructure Resources)     |
       |  - Utilization (CPU/RAM Usage)     -&amp;gt; DASHBOARD       |
       |  - Saturation (Queue/Load Average) -&amp;gt; WARNING/DASHBOARD|
       |  - Errors (Disk I/O, Dropped Pkts) -&amp;gt; DIAGNOSIS       |
       +-------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the infrastructure side (disks, network interfaces, database connection pools), the focus shifts to the USE method. But here is the critical distinction: Utilization alone should rarely be an urgent pager alert. The true focus must be &lt;strong&gt;Saturation&lt;/strong&gt; (queuing/backlog). While 85% disk usage belongs on a dashboard, I/O requests queuing up on disk (I/O saturation) or running out of TCP sockets and dropping packets is an actionable symptom.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Golden Rule: Page Only When Users Are Affected&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The only condition that should wake an on-call engineer from their sleep is when the system cannot meet the promised Service Level Objective (SLO) to users, or when it is certain to fail completely in a short window (e.g., within 1-2 hours, such as a disk filling up at a rapid write rate).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Writing User-Centric Alerts with PromQL
&lt;/h2&gt;

&lt;p&gt;When writing symptom-based rules in Prometheus, instead of relying on simplistic instantaneous thresholds (&lt;code&gt;metric &amp;gt; 90&lt;/code&gt;), calculations should be based on rates and distributions spanning a defined time window. A well-crafted PromQL query must filter out transient spikes (jitter) and confirm the persistence of the failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Error Rate Alert
&lt;/h3&gt;

&lt;p&gt;Instead of a simple error count, the error percentage relative to total traffic should be calculated. To prevent alerts from firing due to a single isolated error during low-traffic periods, a minimum request rate condition is included in the query:&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;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_symptom_alerts&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HighHTTPErrorRate&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;sum(rate(http_requests_total{status=~"^5.."}[5m]))&lt;/span&gt;
            &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="s"&gt;sum(rate(http_requests_total[5m]))&lt;/span&gt;
          &lt;span class="s"&gt;) * 100 &amp;gt; 2&lt;/span&gt;
          &lt;span class="s"&gt;and&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(http_requests_total[5m])) &amp;gt; 10&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
          &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;returning&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5xx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;errors"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2%&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;incoming&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5xx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;minutes.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;printf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;%.2f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}%"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this rule, the &lt;code&gt;for: 3m&lt;/code&gt; clause prevents the on-call engineer from getting paged unnecessarily over momentary 10-second network blips; it requires the issue to persist consistently for at least 3 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Latency / Duration Alert
&lt;/h3&gt;

&lt;p&gt;Percentiles should always be used instead of average latency. An average value masks severe slowdowns experienced by a significant fraction of users. Using the &lt;code&gt;histogram_quantile&lt;/code&gt; function, we measure p99 or p95 latency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HighRequestLatencyP99&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))&lt;/span&gt;
          &lt;span class="s"&gt;&amp;gt; 1.5&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
          &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exceeds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;threshold"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;been&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;above&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1.5s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;minutes.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;value:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;printf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;%.2f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Fast-Depleting Resources (Multi-Window Burn Rate)
&lt;/h3&gt;

&lt;p&gt;For disk space, instead of a static &lt;code&gt;85%&lt;/code&gt; threshold, we use the &lt;code&gt;predict_linear&lt;/code&gt; function to estimate when the disk will run out of space based on its current write rate. This ensures no alerts are generated at night for a disk that will take a month to fill, while fast action is taken for a disk that will fill up in 2 hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DiskFillingFast&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;predict_linear(node_filesystem_free_bytes{mountpoint="/"}[4h], 4 * 3600) &amp;lt; 0&lt;/span&gt;
          &lt;span class="s"&gt;and&lt;/span&gt;
          &lt;span class="s"&gt;node_filesystem_free_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} &amp;lt; 0.2&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
          &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infra&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Root&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;filesystem&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;will&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fill&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;within&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hours"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Root&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;disk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;will&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;completely&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exhausted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;within&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fill&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Filtering Noise with Alertmanager: Routing, Grouping, and Inhibition
&lt;/h2&gt;

&lt;p&gt;Even when Prometheus rules are designed around symptoms, a major infrastructure failure can still trigger hundreds of alerts simultaneously. When a core backbone switch goes down, receiving separate "Service Unreachable" alerts for 40 servers and 200 containers behind it represents peak noise. This is where Alertmanager's grouping and inhibition capabilities become indispensable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJQcm9tZXRoZXVzICg1MCBBbGVydHMgRmlyZWQpIl0gLS0-IEJbIkFsZXJ0bWFuYWdlciJdOwogICAgCiAgICBzdWJncmFwaCAiRmlsdGVyaW5nICYgT3JjaGVzdHJhdGlvbiBFbmdpbmUiCiAgICAgICAgQiAtLT4gQ3siQ2hlY2sgSW5oaWJpdGlvbiBSdWxlcyJ9OwogICAgICAgIEMgLS0-fCJOb2RlRG93biBBY3RpdmU_InwgRFsiTXV0ZSBDb250YWluZXJEb3duIEFsZXJ0cyJdOwogICAgICAgIEMgLS0-fCJObyJ8IEVbIkdyb3VwaW5nIChncm91cF9ieTogW2NsdXN0ZXIsIGFsZXJ0bmFtZV0pIl07CiAgICAgICAgRCAtLT4gRTsKICAgICAgICBFIC0tPiBGWyJUaW1lIFdpbmRvd3MgKGdyb3VwX3dhaXQsIGdyb3VwX2ludGVydmFsKSJdOwogICAgZW5kCiAgICAKICAgIEYgLS0-IEdbIlNpbmdsZSBOb3RpZmljYXRpb24gKFNsYWNrIC8gUGFnZXJEdXR5IC8gT3BzZ2VuaWUpIl07%3Ftype%3Dpng%26bgColor%3Dwhite" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJQcm9tZXRoZXVzICg1MCBBbGVydHMgRmlyZWQpIl0gLS0-IEJbIkFsZXJ0bWFuYWdlciJdOwogICAgCiAgICBzdWJncmFwaCAiRmlsdGVyaW5nICYgT3JjaGVzdHJhdGlvbiBFbmdpbmUiCiAgICAgICAgQiAtLT4gQ3siQ2hlY2sgSW5oaWJpdGlvbiBSdWxlcyJ9OwogICAgICAgIEMgLS0-fCJOb2RlRG93biBBY3RpdmU_InwgRFsiTXV0ZSBDb250YWluZXJEb3duIEFsZXJ0cyJdOwogICAgICAgIEMgLS0-fCJObyJ8IEVbIkdyb3VwaW5nIChncm91cF9ieTogW2NsdXN0ZXIsIGFsZXJ0bmFtZV0pIl07CiAgICAgICAgRCAtLT4gRTsKICAgICAgICBFIC0tPiBGWyJUaW1lIFdpbmRvd3MgKGdyb3VwX3dhaXQsIGdyb3VwX2ludGVydmFsKSJdOwogICAgZW5kCiAgICAKICAgIEYgLS0-IEdbIlNpbmdsZSBOb3RpZmljYXRpb24gKFNsYWNrIC8gUGFnZXJEdXR5IC8gT3BzZ2VuaWUpIl07%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Diagram" width="450" height="1023"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setting up proper &lt;code&gt;inhibit_rules&lt;/code&gt; in Alertmanager prevents downstream cascading alerts from flooding your notification channels when a primary outage occurs.&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="c1"&gt;# alertmanager.yml&lt;/span&gt;
&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;resolve_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;

&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;alertname'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cluster'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;service'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;group_wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
  &lt;span class="na"&gt;group_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
  &lt;span class="na"&gt;repeat_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4h&lt;/span&gt;
  &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default-slack'&lt;/span&gt;
  &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
      &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pagerduty-critical'&lt;/span&gt;
      &lt;span class="na"&gt;continue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
      &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slack-warnings'&lt;/span&gt;

&lt;span class="na"&gt;inhibit_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# If the host is completely down, mute service alerts on that host&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;alertname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NodeDown'&lt;/span&gt;
    &lt;span class="na"&gt;target_match_re&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;alertname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;InstanceDown|HighHTTPErrorRate|HighMemoryUsage'&lt;/span&gt;
    &lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;node'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;instance'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# If the database is unreachable, mute latency alerts for backend services depending on it&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;alertname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PostgresDead'&lt;/span&gt;
    &lt;span class="na"&gt;target_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;backend'&lt;/span&gt;
    &lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;environment'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;group_wait: 30s&lt;/code&gt; waits 30 seconds after the first alert fires to bundle other related alerts in the same group into a single consolidated notification. Meanwhile, &lt;code&gt;repeat_interval: 4h&lt;/code&gt; controls how frequently an unresolved issue re-notifies the channel, avoiding unnecessary spam.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Label Matching in Inhibition Rules&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;equal&lt;/code&gt; parameter in the &lt;code&gt;inhibit_rules&lt;/code&gt; block is critical. If you apply a global inhibition rule without verifying that the source and target alerts share the same &lt;code&gt;instance&lt;/code&gt; or &lt;code&gt;cluster&lt;/code&gt; label, a failure on Host A could accidentally silence a legitimate alert on Host B.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Actionable Alerts and Runbook Architecture
&lt;/h2&gt;

&lt;p&gt;When an alert triggers, the message displayed on the on-call engineer's screen should not merely state a status—it must serve as a concrete remediation guide. Every minute an engineer spends searching for documentation asking "What does this alert mean and what should I do?" directly inflates your Mean Time to Recovery (MTTR).&lt;/p&gt;

&lt;p&gt;Every alert rule must include two essential links: a focused &lt;strong&gt;Grafana Dashboard URL&lt;/strong&gt; to inspect the relevant service metrics and a &lt;strong&gt;Runbook URL&lt;/strong&gt; outlining initial mitigation steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OrderProcessingLagSpike&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;rabbitmq_queue_messages_ready{queue="order_processing"} &amp;gt; 5000&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
          &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-pipeline&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;queue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;backing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unprocessed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;queue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exceeded&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5000&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;minutes.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Consumer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;services&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;may&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;falling&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;behind."&lt;/span&gt;
          &lt;span class="na"&gt;runbook_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://wiki.internal/ops/runbooks/order-queue-lag"&lt;/span&gt;
          &lt;span class="na"&gt;dashboard_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://grafana.internal/d/orders/order-pipeline-metrics?var-queue=order_processing"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A well-designed runbook should answer these 4 questions directly and concisely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;What symptom occurred?&lt;/strong&gt; (e.g., Order queue is backing up; users cannot receive order confirmations.)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;What is the initial verification step?&lt;/strong&gt; (e.g., Check consumer pod logs for OOM kills or connection timeouts.)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;What is the temporary mitigation?&lt;/strong&gt; (e.g., Scale up consumer pods: &lt;code&gt;kubectl scale deployment order-consumer --replicas=10&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Root cause diagnosis:&lt;/strong&gt; (e.g., Inspect payment gateway response times, check for PostgreSQL lock contention.)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Alert Hygiene and Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;Building a symptom-based monitoring infrastructure is not a one-off project; it requires continuous maintenance. Evolving architectures, newly added microservices, or refactored components can quickly render old alert rules obsolete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       +-------------------------------------------------------+
       |                  Weekly Alert Review                  |
       |  - Which top 5 alerts fired most in the last 7 days?  |
       |  - Which alerts required ZERO action?                 |
       +---------------------------+---------------------------+
                                   |
                                   v
       +-------------------------------------------------------+
       |              Classification and Improvement           |
       |  - Non-actionable      -&amp;gt; DELETE or move to Dashboard |
       |  - Incorrect threshold -&amp;gt; Adjust PromQL / Time window |
       |  - Genuine symptom     -&amp;gt; Update Runbook              |
       +-------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To maintain alert hygiene, incorporate these rules into your team's operational routines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The Zero-Action Rule:&lt;/strong&gt; Any alert that fired more than 5 times in the past month without requiring a single code change, service restart, configuration fix, or incident triage must be deleted immediately or converted into a dashboard metric.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Silencing Discipline:&lt;/strong&gt; Temporary silences created in the Alertmanager UI must always include an expiration (TTL) and a link to an active tracking ticket or task. Indefinite silences are the leading cause of blind spots in production.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Alert Review Meetings:&lt;/strong&gt; During weekly on-call handovers, review the total alert count and noise ratio. Any rule that woke up the on-call engineer unnecessarily should be revised immediately via a pull request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Achieving sustainable observability with Prometheus and Alertmanager isn't about writing more alert rules; it's about monitoring the right architectural layer with the right methodology. Instead of drowning in the noise of thousands of cause-based rules, implementing symptom-based alerting grounded in RED and USE principles restores confidence for on-call teams.&lt;/p&gt;

&lt;p&gt;Keeping infrastructure metrics (CPU, RAM, connection counts) on Grafana dashboards as diagnostic aids when symptoms fire—while reserving pages exclusively for moments when the user experience is genuinely compromised—is the foundation of operational resilience.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dzone.com/articles/red-method-for-prometheus-3-key-metrics-for-micros" rel="noopener noreferrer"&gt;dzone.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rancher.cn/red-method-for-prometheus-3-key-metrics-for-monitoring" rel="noopener noreferrer"&gt;rancher.cn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/blog/the-red-method-how-to-instrument-your-services/" rel="noopener noreferrer"&gt;grafana.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-02-09-sli-red-use-methods/view" rel="noopener noreferrer"&gt;oneuptime.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/prometheus/latest/querying/functions/" rel="noopener noreferrer"&gt;Query functions - Prometheus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/alerting/latest/configuration/" rel="noopener noreferrer"&gt;Configuration - Prometheus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>observability</category>
      <category>sistemmimarisi</category>
      <category>software</category>
    </item>
    <item>
      <title>Introduction to GitOps: Pull-Based Deployment Model with Argo CD</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Sun, 16 Aug 2026 00:01:36 +0000</pubDate>
      <link>https://dev.to/merbayerp/introduction-to-gitops-pull-based-deployment-model-with-argo-cd-403f</link>
      <guid>https://dev.to/merbayerp/introduction-to-gitops-pull-based-deployment-model-with-argo-cd-403f</guid>
      <description>&lt;h2&gt;
  
  
  What is GitOps?
&lt;/h2&gt;

&lt;p&gt;GitOps defines the principle of &lt;strong&gt;managing infrastructure and application configurations by using a Git repository as the single source of truth&lt;/strong&gt;. This approach minimizes human error through declarative definitions and automated synchronization. The core advantage of GitOps is that every change is fully traceable and reversible by the entire team; when a commit is rolled back, the system can automatically revert to its previous desired state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Why GitOps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitOps combines change auditing with Git's robust version control mechanism, significantly simplifying auditing, compliance, and rollback processes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How Does Argo CD Work?
&lt;/h2&gt;

&lt;p&gt;Argo CD is a &lt;strong&gt;controller that monitors Git repositories and automatically pulls and applies application manifests to a Kubernetes cluster&lt;/strong&gt;. In the Pull-Based model, Argo CD defines the target repository and path via an &lt;code&gt;Application&lt;/code&gt; custom resource; the control loop checks the repository every 3 minutes (180 seconds) by default and initiates a synchronization if differences are detected. This process displays real-time &lt;code&gt;sync&lt;/code&gt; and &lt;code&gt;health&lt;/code&gt; statuses directly in the Argo CD UI.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example/demo&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manifests&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;Application&lt;/code&gt; manifest above, setting &lt;code&gt;spec.syncPolicy.automated.prune: true&lt;/code&gt; ensures that resources deleted from the Git repository are also automatically removed from the Kubernetes cluster. Setting &lt;code&gt;selfHeal: true&lt;/code&gt; ensures that when drift from the state defined in Git is detected within the live cluster, Argo CD automatically reverts those changes to keep the cluster state matched with Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Pull-Based Deployment Model?
&lt;/h2&gt;

&lt;p&gt;The Pull-Based deployment model refers to &lt;strong&gt;the control plane (controller) pulling and applying configurations from a Git repository independently of manual user interaction&lt;/strong&gt;. In this model, instead of push commands like &lt;code&gt;kubectl apply&lt;/code&gt;, an operator like Argo CD periodically reads the repository and automatically synchronizes differences. The pull approach simplifies firewall rules because it requires only an outbound connection and eliminates the need to expose an external listener within your internal network, significantly reducing security risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Argo CD Installation Steps
&lt;/h2&gt;

&lt;p&gt;Installation can be done using Helm charts or &lt;code&gt;kubectl&lt;/code&gt; manifests. Here is a simple example using &lt;code&gt;kubectl&lt;/code&gt;. The first step is to create the &lt;code&gt;argocd&lt;/code&gt; namespace and apply the necessary manifests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Production Environment Warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;kubectl apply&lt;/code&gt; command below installs Argo CD with cluster administrator (&lt;code&gt;cluster-admin&lt;/code&gt;) privileges and is a non-HA (High Availability) setup, which is not recommended for production environments. For production, installing via Helm and pinning to a specific Argo CD version is recommended. Additionally, rather than applying manifests directly from &lt;code&gt;https://raw.githubusercontent.com&lt;/code&gt;, downloading, inspecting, and then applying them is much safer.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create namespace&lt;/span&gt;
kubectl create namespace argocd

&lt;span class="c"&gt;# Apply Argo CD manifests&lt;/span&gt;
&lt;span class="c"&gt;# --server-side and --force-conflicts flags are recommended due to CRD size limits.&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; argocd &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="nt"&gt;--force-conflicts&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, configure a &lt;code&gt;LoadBalancer&lt;/code&gt; or &lt;code&gt;Ingress&lt;/code&gt; to expose the &lt;code&gt;argocd-server&lt;/code&gt; service externally. Once the installation is complete, the &lt;code&gt;admin&lt;/code&gt; password is stored directly inside a secret, allowing you to log in to the UI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Port Forwarding for Local Testing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This command is used to access the Argo CD UI from your local machine and should not be used in production environments. The terminal session will remain attached while the command runs.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Port forward for UI access (for local testing)&lt;/span&gt;
kubectl port-forward svc/argocd-server &lt;span class="nt"&gt;-n&lt;/span&gt; argocd 8080:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installation, you can authenticate via the CLI using &lt;code&gt;argocd login&lt;/code&gt;; this allows you to bypass the UI in script-based automations. For Argo CD v1.9 and later, the initial admin password is stored in the &lt;code&gt;argocd-initial-admin-secret&lt;/code&gt; secret under the &lt;code&gt;password&lt;/code&gt; key.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Security Warning: Change the Default Password&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is strongly recommended to change the default &lt;code&gt;admin&lt;/code&gt; password immediately after your first login. You can also delete the &lt;code&gt;argocd-initial-admin-secret&lt;/code&gt; secret once the password has been updated.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Retrieve the admin password (for Argo CD v1.9+)&lt;/span&gt;
&lt;span class="nv"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd get secret argocd-initial-admin-secret &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{.data.password}"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Argo CD Admin Password: &lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Authenticate via CLI (--insecure flag may be required for local testing)&lt;/span&gt;
argocd login localhost:8080 &lt;span class="nt"&gt;--username&lt;/span&gt; admin &lt;span class="nt"&gt;--password&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--insecure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Application Deployment Workflow
&lt;/h2&gt;

&lt;p&gt;The Mermaid diagram below shows how a Git commit reaches Kubernetes through Argo CD. This workflow includes the &lt;strong&gt;detect change → synchronize → health check → report&lt;/strong&gt; steps. Logs are recorded at each phase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJHaXQgUmVwb3NpdG9yeSJdIC0tPiBCWyJBcmdvIENEIChDb250cm9sbGVyKSJdOwogICAgQiAtLT4gQ1siS3ViZXJuZXRlcyBBUEkgU2VydmVyIl07CiAgICBDIC0tPiBEWyJQb2QgLyBTZXJ2aWNlIC8gRGVwbG95bWVudCJdOwogICAgQiAtLT4gRVsiQXJnbyBDRCBVSSJdOwogICAgRSAtLT4gRlsiSGVhbHRoICYgU3luYyBTdGF0dXMiXTs%3Ftype%3Dpng%26bgColor%3Dwhite" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJHaXQgUmVwb3NpdG9yeSJdIC0tPiBCWyJBcmdvIENEIChDb250cm9sbGVyKSJdOwogICAgQiAtLT4gQ1siS3ViZXJuZXRlcyBBUEkgU2VydmVyIl07CiAgICBDIC0tPiBEWyJQb2QgLyBTZXJ2aWNlIC8gRGVwbG95bWVudCJdOwogICAgQiAtLT4gRVsiQXJnbyBDRCBVSSJdOwogICAgRSAtLT4gRlsiSGVhbHRoICYgU3luYyBTdGF0dXMiXTs%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Diagram" width="531" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a production-like environment, changes can be inspected beforehand using the &lt;code&gt;dry-run&lt;/code&gt; option during sync. In the Argo CD CLI, running &lt;code&gt;argocd app sync --dry-run&lt;/code&gt; executes a client-side dry-run by default. For a server-side dry-run, additional configuration or an alternative approach may be needed.&lt;/p&gt;

&lt;p&gt;To reproduce this workflow in a lab environment, simply create an &lt;code&gt;Application&lt;/code&gt; manifest and replace the &lt;code&gt;repoURL&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt; fields with your own test repository. After synchronization, you can verify that the deployment succeeded by running &lt;code&gt;kubectl get pods -n &amp;lt;namespace&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices and Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Practice 1 – Declarative Manifests:&lt;/strong&gt; Make manifest files as parameter-driven as possible; overlays based on &lt;code&gt;kustomize&lt;/code&gt; or &lt;code&gt;helm&lt;/code&gt; allow you to manage environment differences (dev, staging, prod) within a single repository. &lt;strong&gt;Common Pitfall 1 – Direct Pushes:&lt;/strong&gt; Direct push operations like &lt;code&gt;kubectl apply&lt;/code&gt; defeat the entire purpose of GitOps and stop Git from being the single source of truth; route all changes exclusively through Git.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Practice 2 – Automated Self-Heal:&lt;/strong&gt; The &lt;code&gt;syncPolicy.automated.selfHeal&lt;/code&gt; feature automatically remediates drift (state divergence) without requiring manual intervention. &lt;strong&gt;Common Pitfall 2 – Prune Disabled:&lt;/strong&gt; Failing to clean up deleted resources automatically leads to resource leakage over time; enabling &lt;code&gt;prune: true&lt;/code&gt; eliminates this risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Practice 3 – RBAC Configuration:&lt;/strong&gt; Using the default &lt;code&gt;admin&lt;/code&gt; account in production creates a serious security vulnerability; define project-based roles instead and grant access only to the necessary namespaces. &lt;strong&gt;Common Pitfall 3 – Overly Broad Permissions:&lt;/strong&gt; Granting broad &lt;code&gt;cluster-admin&lt;/code&gt; roles can cause a single faulty commit to impact the entire cluster; strictly enforce the principle of least privilege.&lt;/p&gt;

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

&lt;p&gt;Argo CD &lt;strong&gt;puts the pull-based model of GitOps into practice, making Kubernetes deployments consistent and observable&lt;/strong&gt;. Declarative manifests, automated self-healing, and granular RBAC controls ensure the long-term sustainability of this approach. Because the setup and core workflow are straightforward, teams can quickly transition to production; however, if security and resource management practices are neglected, the benefits of GitOps can quickly erode. The next natural step is to integrate your existing CI pipeline with Argo CD and introduce post-sync automated testing and canary deployment strategies.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-01-30-pull-based-deployment/view" rel="noopener noreferrer"&gt;oneuptime.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/open-cluster-management-io/argocd-pull-integration" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.redhat.com/en/topics/devops/what-is-gitops" rel="noopener noreferrer"&gt;redhat.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ibm.com/think/topics/gitops" rel="noopener noreferrer"&gt;ibm.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cncf.io/blog/2021/09/28/gitops-101-whats-it-all-about/" rel="noopener noreferrer"&gt;cncf.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.plural.sh/blog/what-is-gitops/" rel="noopener noreferrer"&gt;plural.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://argo-cd.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;readthedocs.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://circleci.com/blog/what-is-argo-cd/" rel="noopener noreferrer"&gt;circleci.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Lightweight Kubernetes with k3s: Scaling from Single Node</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Sat, 15 Aug 2026 13:46:34 +0000</pubDate>
      <link>https://dev.to/merbayerp/lightweight-kubernetes-with-k3s-scaling-from-single-node-ldj</link>
      <guid>https://dev.to/merbayerp/lightweight-kubernetes-with-k3s-scaling-from-single-node-ldj</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While working on a production ERP system, we needed a fast and flexible container management system. To meet this need, we chose k3s. k3s is a lightweight Kubernetes distribution. In this article, we will explain how to scale from a single node to multiple nodes using k3s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lightweight Kubernetes with k3s
&lt;/h2&gt;

&lt;p&gt;k3s is an open-source project supported by the Cloud Native Computing Foundation (CNCF). k3s is lighter and consumes fewer resources compared to traditional Kubernetes distributions, making it ideal for resource-constrained environments or edge computing scenarios. Therefore, k3s is ideal for use in small and medium-sized projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Node Deployment with k3s
&lt;/h3&gt;

&lt;p&gt;Deploying k3s as a single node is quite simple. The first step is to create a server and install k3s on it. After installing k3s, you can create a cluster and run containers on this cluster.&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;# To install the k3s server&lt;/span&gt;
&lt;span class="c"&gt;# WARNING: Running scripts downloaded from the internet can pose a security risk.&lt;/span&gt;
&lt;span class="c"&gt;# In production environments, it is recommended to review the script or use alternative installation methods.&lt;/span&gt;
curl &lt;span class="nt"&gt;-sfL&lt;/span&gt; https://get.k3s.io | sh -

&lt;span class="c"&gt;# To check the status of the k3s cluster&lt;/span&gt;
k3s kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command installs the k3s server and loads helper programs like &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;crictl&lt;/code&gt;, &lt;code&gt;ctr&lt;/code&gt;. The Kubeconfig file is written to &lt;code&gt;/etc/rancher/k3s/k3s.yaml&lt;/code&gt;, and the &lt;code&gt;k3s kubectl&lt;/code&gt; command automatically uses this file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to Multiple Nodes
&lt;/h2&gt;

&lt;p&gt;Scaling from a single server node to multiple nodes is particularly easy with k3s by adding worker (agent) nodes. This allows you to increase the capacity of your cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Worker Nodes
&lt;/h3&gt;

&lt;p&gt;To add worker nodes, you first need to create these nodes. Then, you can install the k3s agent on them and have them join the existing k3s server cluster.&lt;/p&gt;

&lt;p&gt;To have worker nodes join the cluster, you need to obtain a token from your k3s server node. This token is found in the &lt;code&gt;/var/lib/rancher/k3s/server/node-token&lt;/code&gt; file on the server node.&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;# Get the token from the server node (e.g., if the server IP is 192.168.1.100)&lt;/span&gt;
&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /var/lib/rancher/k3s/server/node-token

&lt;span class="c"&gt;# To install the k3s agent on worker nodes and join the cluster&lt;/span&gt;
&lt;span class="c"&gt;# Replace &amp;lt;SERVER_IP&amp;gt; with the IP address of your k3s server and &amp;lt;NODE_TOKEN&amp;gt; with the token you obtained above.&lt;/span&gt;
&lt;span class="c"&gt;# WARNING: Running scripts downloaded from the internet can pose a security risk.&lt;/span&gt;
&lt;span class="c"&gt;# In production environments, it is recommended to review the script or use alternative installation methods.&lt;/span&gt;
curl &lt;span class="nt"&gt;-sfL&lt;/span&gt; https://get.k3s.io | &lt;span class="nv"&gt;K3S_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://&amp;lt;SERVER_IP&amp;gt;:6443 &lt;span class="nv"&gt;K3S_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;NODE_TOKEN&amp;gt; sh -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command configures the new node as a k3s agent and registers it with the specified server. After the agent joins successfully, you can verify that the new worker node is visible in the cluster by running &lt;code&gt;k3s kubectl get nodes&lt;/code&gt; from your server node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;When creating a lightweight Kubernetes deployment with k3s, it is crucial not to neglect security. The first step is to secure your servers. Then, you can secure your k3s cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server Security
&lt;/h3&gt;

&lt;p&gt;To secure your servers, you first need to keep them updated regularly. Then, you can protect them with a firewall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# To update your servers&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# To protect your servers with a firewall (e.g., using UFW)&lt;/span&gt;
&lt;span class="c"&gt;# WARNING: Before enabling the firewall, ensure that you have opened the necessary ports for Kubernetes (e.g., 6443, 10250),&lt;/span&gt;
&lt;span class="c"&gt;# or cluster communication may be disrupted.&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  k3s Security
&lt;/h3&gt;

&lt;p&gt;To secure your k3s cluster, it is essential to keep your k3s version up to date, as updates include security patches and improvements. To update k3s, you can rerun the installation script with the same configuration options or manually update the binary.&lt;/p&gt;

&lt;p&gt;Additionally, you can apply Kubernetes Network Policies to control traffic between pods. k3s includes a built-in network policy controller by default. Network policies allow you to restrict communication between pods, enabling a zero-trust network model.&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;# To check the k3s version&lt;/span&gt;
k3s &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;span class="c"&gt;# To update k3s (preserving the existing configuration)&lt;/span&gt;
&lt;span class="c"&gt;# WARNING: Before updating, it is recommended to back up your data and update server nodes before agent nodes.&lt;/span&gt;
&lt;span class="c"&gt;# Also, avoid large version jumps by updating one minor version at a time.&lt;/span&gt;
curl &lt;span class="nt"&gt;-sfL&lt;/span&gt; https://get.k3s.io | sh -

&lt;span class="c"&gt;# To create an example network policy (this only creates a template, you need to define its content)&lt;/span&gt;
&lt;span class="c"&gt;# This command alone does not provide security; the content of the NetworkPolicy YAML file is important.&lt;/span&gt;
&lt;span class="c"&gt;# kubectl create networkpolicy &amp;lt;policy-name&amp;gt; --from=&amp;lt;source&amp;gt; --to=&amp;lt;target&amp;gt; --port=&amp;lt;port&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# For example, to create a policy that denies all traffic between pods by default:&lt;/span&gt;
&lt;span class="c"&gt;# kubectl apply -f default-deny-all.yaml&lt;/span&gt;
&lt;span class="c"&gt;# (default-deny-all.yaml content: https://docs.k3s.io/security/hardening-guide#network-policies)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;In this article, we explained how to create a lightweight Kubernetes deployment using k3s and scale it from a single node to multiple nodes by adding worker nodes. k3s is a lightweight and flexible Kubernetes distribution ideal for small and medium-sized projects. When creating a lightweight Kubernetes deployment with k3s, do not neglect security; take measures such as regular updates and network policies for both server and cluster security. The next step is to deploy this distribution in a production environment and use it in a real project.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-03-20-k3s-install-single-node/view" rel="noopener noreferrer"&gt;oneuptime.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://loganmarchione.com/2022/03/k3s-single-node-cluster-for-noobs/" rel="noopener noreferrer"&gt;loganmarchione.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/wim-vdw/k3s-ha-setup" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sysdig.com/learn-cloud-native/what-is-k3s" rel="noopener noreferrer"&gt;sysdig.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-03-20-k3s-manual-upgrade/view" rel="noopener noreferrer"&gt;oneuptime.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://veritasautomata.com/insights/thought-leadership/security-best-practices-for-k3s-in-distributed-environments/" rel="noopener noreferrer"&gt;veritasautomata.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rancher.com/products/k3s" rel="noopener noreferrer"&gt;rancher.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hashbang.nl/blog/upgrading-old-k3s-cluster-learnings" rel="noopener noreferrer"&gt;hashbang.nl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>guide</category>
      <category>software</category>
    </item>
    <item>
      <title>Setting Up Your Own Tailscale Control Server with Headscale</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Sat, 15 Aug 2026 08:09:42 +0000</pubDate>
      <link>https://dev.to/merbayerp/setting-up-your-own-tailscale-control-server-with-headscale-31pl</link>
      <guid>https://dev.to/merbayerp/setting-up-your-own-tailscale-control-server-with-headscale-31pl</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Headscale is an open-source application of the Tailscale control server, allowing users to set up their own Tailscale-compatible control servers. This provides more flexibility in managing your mesh-VPN network and customizing security settings. In this article, you will learn the steps to set up your own Tailscale control server with Headscale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Headscale?
&lt;/h2&gt;

&lt;p&gt;Headscale is an open-source application of the Tailscale control server. Tailscale is a modern VPN solution built on WireGuard, enabling users to securely access remote devices. While Tailscale's managed service does not offer a self-hosted control server option, Headscale provides an open-source alternative for self-hosting and hobbyist users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your Own Tailscale Control Server with Headscale
&lt;/h2&gt;

&lt;p&gt;To set up your own Tailscale control server with Headscale, follow these steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Headscale
&lt;/h3&gt;

&lt;p&gt;To install Headscale, you first need a server or virtual machine. After setting up your server or virtual machine, you can install Headscale. Headscale is officially available on Docker Hub and can be installed using Docker.&lt;/p&gt;

&lt;p&gt;In production environments, it is recommended to use a specific version instead of the 'latest' tag. The following command pulls the latest stable version of Headscale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull headscale/headscale:0.23.0 &lt;span class="c"&gt;# Example version, check the current stable version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Instead of the 'latest' tag, it is recommended to always use the current stable version number (e.g., &lt;code&gt;0.23.0&lt;/code&gt;) from Headscale's official GitHub release page when pulling the Docker image. This helps avoid unexpected changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure Headscale
&lt;/h3&gt;

&lt;p&gt;After installing Headscale, you can configure it using the following commands. This command runs Headscale on port 8080 by default and mounts its configuration to the &lt;code&gt;/etc/headscale&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;First, you need to create and edit the Headscale configuration file (e.g., &lt;code&gt;config.yaml&lt;/code&gt;) on your server. Headscale looks for the configuration file in &lt;code&gt;/etc/headscale&lt;/code&gt;, &lt;code&gt;$HOME/.headscale&lt;/code&gt;, or the current working directory. You can download an example configuration file from the Headscale GitHub repository.&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;# Example: Create the configuration directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/headscale &lt;span class="c"&gt;# Or your preferred directory&lt;/span&gt;

&lt;span class="c"&gt;# Example: Download the current configuration example from Headscale's GitHub repository&lt;/span&gt;
&lt;span class="c"&gt;# Replace HEADSCALE_VERSION with the current stable version (e.g., 0.23.0)&lt;/span&gt;
&lt;span class="c"&gt;# wget -O /var/lib/headscale/config.yaml https://github.com/juanfont/headscale/raw/v${HEADSCALE_VERSION}/config-example.yaml&lt;/span&gt;
&lt;span class="c"&gt;# nano /var/lib/headscale/config.yaml # Edit the configuration file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, start Headscale with Docker:&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;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; headscale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/lib/headscale:/etc/headscale &lt;span class="se"&gt;\&lt;/span&gt;
  headscale/headscale:0.23.0 &lt;span class="c"&gt;# Example version, use the current stable version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command runs Headscale on port 8080 and mounts its configuration from &lt;code&gt;/var/lib/headscale&lt;/code&gt; to &lt;code&gt;/etc/headscale&lt;/code&gt; in the container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Install Tailscale
&lt;/h3&gt;

&lt;p&gt;After installing Headscale, you can install the Tailscale client. Tailscale has different installation methods for various platforms. For more information, refer to the Tailscale documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Connect Tailscale to Headscale
&lt;/h3&gt;

&lt;p&gt;After installing the Tailscale client, you can connect it to your Headscale server using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale up &lt;span class="nt"&gt;--login-server&lt;/span&gt; http://&amp;lt;HEADSCALE_SERVER_ADDRESS_OR_IP&amp;gt;:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command connects the Tailscale client to your Headscale server. Replace &lt;code&gt;&amp;lt;HEADSCALE_SERVER_ADDRESS_OR_IP&amp;gt;&lt;/code&gt; with the actual IP address or domain name of your Headscale server. For example, if your Headscale server is running at &lt;code&gt;myheadscale.example.com&lt;/code&gt;, the command would be &lt;code&gt;tailscale up --login-server http://myheadscale.example.com:8080&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Your Own Tailscale Control Server with Headscale
&lt;/h2&gt;

&lt;p&gt;After setting up your own Tailscale control server with Headscale, you can use Headscale to manage your mesh-VPN network. Headscale provides full control over the control plane, in addition to many features found in Tailscale's managed service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Settings
&lt;/h3&gt;

&lt;p&gt;Headscale offers similar security options to Tailscale and provides full control over your environment. For example, with Headscale, you can use your own SSL/TLS certificates and manage TLS configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Management
&lt;/h3&gt;

&lt;p&gt;Headscale provides various tools for managing your network. For example, with Headscale, you can manage access control lists (ACLs), control node registration, and customize your network security settings.&lt;/p&gt;

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

&lt;p&gt;Setting up your own Tailscale control server with Headscale provides more flexibility and control in managing your mesh-VPN network and customizing security settings. In this article, you learned the steps to set up your own Tailscale control server with Headscale. Headscale is an open-source application of the Tailscale control server, allowing users to self-host their own Tailscale-compatible networks.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.lucasjanin.com/2025/01/03/headscale-tailscale-in-a-self-hosted-environment/" rel="noopener noreferrer"&gt;lucasjanin.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/juanfont/headscale" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/juanfont/headscale/blob/main/docs/ref/configuration.md" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/docs/reference/tailscale-cli/up" rel="noopener noreferrer"&gt;tailscale.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/docs/reference/tailscale-cli" rel="noopener noreferrer"&gt;tailscale.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lab.uberspace.de/guide_headscale/" rel="noopener noreferrer"&gt;uberspace.de&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://headscale.net/" rel="noopener noreferrer"&gt;headscale.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/docs" rel="noopener noreferrer"&gt;tailscale.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vpn</category>
      <category>guide</category>
      <category>software</category>
    </item>
    <item>
      <title>Syncthing for Cloudless Synchronization: Conflicts and Versions</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Sat, 15 Aug 2026 03:06:51 +0000</pubDate>
      <link>https://dev.to/merbayerp/syncthing-for-cloudless-synchronization-conflicts-and-versions-4jn1</link>
      <guid>https://dev.to/merbayerp/syncthing-for-cloudless-synchronization-conflicts-and-versions-4jn1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While working on a production ERP, I experienced issues with file synchronization. Instead of storing files in cloud services, we preferred to store them on our own servers. However, this led to some problems with file synchronization. To address these issues, we decided to use Syncthing. Syncthing is a tool that provides cloudless synchronization. In this article, I will discuss the advantages and disadvantages of using Syncthing, as well as what can be done regarding conflicts and versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages of Syncthing
&lt;/h2&gt;

&lt;p&gt;Syncthing has several advantages when it comes to file synchronization. Firstly, it is not dependent on cloud services; your data is stored only on your devices, and there is no central server. This provides more control over file storage and synchronization. Additionally, Syncthing's source code is open, and it is licensed under the Mozilla Public License 2.0 (MPLv2), making it verifiable and reliable for developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disadvantages of Syncthing
&lt;/h2&gt;

&lt;p&gt;Syncthing also has some disadvantages. The initial setup and configuration, especially regarding network settings, can be somewhat complicated at first. Moreover, for Syncthing to synchronize files between devices, these devices need to be on the same local network or an internet-connected network. If devices are on different networks, direct connection may require port forwarding or the use of relay servers, which can result in lower performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflicts and Versions
&lt;/h2&gt;

&lt;p&gt;Syncthing supports file versioning. This feature allows tracking the history of changes made to a file and can store older versions in a &lt;code&gt;.stversions&lt;/code&gt; folder using strategies like "Simple File Versioning."&lt;/p&gt;

&lt;p&gt;In the event of a conflict, Syncthing does not automatically resolve it; instead, it preserves both conflicting files. When a file is modified simultaneously on multiple devices and the contents differ, Syncthing recognizes the conflict and renames one of the files to &lt;code&gt;filename.sync-conflict-&amp;lt;date&amp;gt;-&amp;lt;time&amp;gt;-&amp;lt;changingDeviceID&amp;gt;.&amp;lt;extension&amp;gt;&lt;/code&gt;. Typically, the file with the older modification time is marked as the conflict and renamed. This allows the user to have both versions and manually decide which one is the "best" version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;syncthing &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above command will display Syncthing's version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings and Configuration
&lt;/h2&gt;

&lt;p&gt;When you start Syncthing for the first time, it automatically generates a device ID and a default configuration file. Syncthing does not use a central account system; devices recognize and pair with each other through unique device IDs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;syncthing generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above command generates a default configuration file (&lt;code&gt;config.xml&lt;/code&gt;) and cryptographic keys for Syncthing. &lt;strong&gt;Warning:&lt;/strong&gt; If Syncthing is already running or there is an existing configuration file, this command will create a new one and overwrite the existing file. To update or change an existing configuration, it's usually preferable to use Syncthing's web-based graphical user interface (GUI) or edit the &lt;code&gt;config.xml&lt;/code&gt; file directly.&lt;/p&gt;

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

&lt;p&gt;Syncthing is a cloudless, open-source, and secure file synchronization tool. It has advantages and disadvantages. It supports file versioning and manages conflicts by creating &lt;code&gt;sync-conflict&lt;/code&gt; files, allowing the user to manually resolve them. To use Syncthing, there's no need to create a central account; instead, device IDs are used for pairing, and the configuration file is automatically generated on the first run or can be manually generated with the &lt;code&gt;syncthing generate&lt;/code&gt; command. The next step is to start using Syncthing and synchronize files by pairing devices and configuring folders.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/syncthing/syncthing" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forum.syncthing.net/t/syncthing-is-now-mplv2-licensed/2133" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.mavnn.eu/2025/08/15/conflict_free_syncthing_notes.html" rel="noopener noreferrer"&gt;mavnn.co.uk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/v1.0.0/users/faq.html" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/users/firewall.html" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/v1.19.0/users/versioning.html" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/v1.0.0/users/versioning.html" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/users/versioning.html" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>guide</category>
      <category>software</category>
    </item>
    <item>
      <title>Code Review Culture: Small PRs, Fast Flow, Low Friction</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Sat, 15 Aug 2026 01:10:08 +0000</pubDate>
      <link>https://dev.to/merbayerp/code-review-culture-small-prs-fast-flow-low-friction-3fmc</link>
      <guid>https://dev.to/merbayerp/code-review-culture-small-prs-fast-flow-low-friction-3fmc</guid>
      <description>&lt;h2&gt;
  
  
  Code Review Culture
&lt;/h2&gt;

&lt;p&gt;Code review is a critical process that enables a software team to collaborate and produce high-quality code. Building a solid code review culture helps boost team collaboration and efficiency. Small PRs (Pull Requests) and a fast flow are essential to making the review process much more effective. In this post, we will explore the importance of code review culture and what you can do to maintain a fast flow through small PRs.&lt;/p&gt;

&lt;p&gt;Code reviews impact not just code quality, but also the way a team collaborates. Establishing a healthy review process helps build mutual trust among team members. Furthermore, code reviews help onboard new members quickly and accelerate their learning curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small PRs
&lt;/h2&gt;

&lt;p&gt;Small PRs are vital for keeping the code review process efficient. They allow the code to be split into bite-sized pieces, making it possible to review each part thoroughly on its own. This improves overall code quality and makes bugs significantly easier to catch. Small PRs get reviewed faster, generate better feedback, and are much easier to revert if something goes wrong.&lt;/p&gt;

&lt;p&gt;To create small PRs, pick an isolated part of the codebase and turn it into its own dedicated pull request. For instance, small changes such as refactoring a single function or introducing a small feature should each live in their own PRs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example of creating a PR
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;

&lt;span class="c1"&gt;# Modified code
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Length and width must be greater than zero&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fast Flow
&lt;/h2&gt;

&lt;p&gt;A fast flow is crucial for making the code review process efficient. It ensures that code gets reviewed and approved without unnecessary delays. This enhances communication across the team and raises the overall quality of the code.&lt;/p&gt;

&lt;p&gt;To achieve a fast flow, you can automate parts of the code review process. For example, GitHub Actions is commonly used to automate review checks and CI pipelines on GitHub.&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="c1"&gt;# Example GitHub Actions workflow&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Code Review&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt; &lt;span class="c1"&gt;# Current and recommended version is v4.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run code review&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Code review commands&lt;/span&gt;
          &lt;span class="s"&gt;echo "Code review completed successfully"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Low Friction
&lt;/h2&gt;

&lt;p&gt;Low friction is key to maintaining an efficient code review process. Minimizing friction helps code get reviewed and approved swiftly, improving communication across the team and elevating code quality.&lt;/p&gt;

&lt;p&gt;To achieve low friction, it is essential to keep the code review process open, clear, and transparent. For instance, you can establish clear documentation outlining the review guidelines and make it easily accessible to everyone on the team.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Code Review Process&lt;/span&gt;
&lt;span class="gu"&gt;## Step 1: Code Review&lt;/span&gt;
The code review phase is essential for improving code quality.
&lt;span class="gu"&gt;## Step 2: Code Approval&lt;/span&gt;
The approval phase ensures the code meets standards before merging.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A strong code review culture is a vital practice that helps software teams collaborate effectively and produce high-quality code. Small PRs, fast flow, and low friction are the core pillars of an efficient review process. In this post, we covered why code review culture matters and how keeping PRs small enables continuous flow. Structuring the review process in an open and transparent manner strengthens team communication and consistently elevates the quality of your code.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devcom.com/tech-blog/12-code-review-best-practices-how-to-do-effective-code-reviews/" rel="noopener noreferrer"&gt;devcom.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/pull-requests/concepts/helping-others-review-your-changes" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/eng-practices/review/reviewer/standard.html" rel="noopener noreferrer"&gt;github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/eng-practices/review/" rel="noopener noreferrer"&gt;github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://microsoft.github.io/code-with-engineering-playbook/code-reviews/process-guidance/reviewer-guidance/" rel="noopener noreferrer"&gt;github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.codacy.com/small-pull-requests" rel="noopener noreferrer"&gt;codacy.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://testing.googleblog.com/2024/07/in-praise-of-small-pull-requests.html" rel="noopener noreferrer"&gt;googleblog.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>indiehacker</category>
    </item>
    <item>
      <title>Ending SSH Key Chaos: Setting Up Your Own SSH Certificate Authority</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Fri, 14 Aug 2026 13:46:24 +0000</pubDate>
      <link>https://dev.to/merbayerp/ending-ssh-key-chaos-setting-up-your-own-ssh-certificate-authority-3fdg</link>
      <guid>https://dev.to/merbayerp/ending-ssh-key-chaos-setting-up-your-own-ssh-certificate-authority-3fdg</guid>
      <description>&lt;h1&gt;
  
  
  Ending SSH Key Chaos: Setting Up Your Own SSH Certificate Authority
&lt;/h1&gt;

&lt;p&gt;SSH keys often create conflicts and revision problems when managing server access; you can eliminate this chaos by setting up your own SSH Certificate Authority (CA).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does SSH Chaos Occur?
&lt;/h2&gt;

&lt;p&gt;Manually distributing SSH keys leads to inconsistencies and the problem of not removing old keys.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate keys&lt;/strong&gt;: When the same key is copied to multiple servers, access remains even if an employee's account is deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation delay&lt;/strong&gt;: It can take hours to collect all keys when an employee leaves, creating a security vulnerability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These problems can be reduced by creating a &lt;strong&gt;central signing authority&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Why CA?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CA signs multiple host certificates with a single root private key; clients only need to trust the CA's public key, thus eliminating individual key management.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is an SSH CA and How Does it Work?
&lt;/h2&gt;

&lt;p&gt;An SSH CA is a certificate model supported by OpenSSH, similar to X.509 certificates but in its own format. Host or user certificates are generated using &lt;code&gt;ssh-keygen -s&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root key&lt;/strong&gt;: The CA's private key is stored in one place and has very limited access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed certificates&lt;/strong&gt;: The certificate given to the server carries the CA's signature and matches files like &lt;code&gt;HostCertificate&lt;/code&gt;. The certificate given to the user is trusted by the server via the &lt;code&gt;TrustedUserCAKeys&lt;/code&gt; directive and can have user-specific access restrictions via files like &lt;code&gt;AuthorizedPrincipalsFile&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup creates a &lt;strong&gt;trust chain&lt;/strong&gt;: the client accepts the CA public key, verifies the host certificate signed by the CA, and grants access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow Summary Graph
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7IEFbIlNTSCBDbGllbnQiXSAtLT4gQlsiQ0EgKFJvb3QgUHJpdmF0ZSBLZXkpIl07IEIgLS0-IENbIlNlcnZlciBIb3N0IENlcnRpZmljYXRlIl07IEMgLS0-IERbIlNlcnZlciBTU0ggRGFlbW9uIl07IEQgLS0-IEFbIkNvbm5lY3Rpb24iXQ%3Ftype%3Dpng%26bgColor%3Dwhite" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7IEFbIlNTSCBDbGllbnQiXSAtLT4gQlsiQ0EgKFJvb3QgUHJpdmF0ZSBLZXkpIl07IEIgLS0-IENbIlNlcnZlciBIb3N0IENlcnRpZmljYXRlIl07IEMgLS0-IERbIlNlcnZlciBTU0ggRGFlbW9uIl07IEQgLS0-IEFbIkNvbm5lY3Rpb24iXQ%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Diagram" width="304" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the CA Root Key
&lt;/h2&gt;

&lt;p&gt;The root key should be created in a high-security directory and only accessible by &lt;code&gt;root&lt;/code&gt; or via &lt;code&gt;sudo&lt;/code&gt; privileges.&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;# Create a 4096-bit RSA key&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/ssh/ca_key &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"My SSH CA"&lt;/span&gt;
&lt;span class="c"&gt;# Save the public key to /etc/ssh/ca_key.pub&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/ssh/ca_key
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;644 /etc/ssh/ca_key.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt;: Using an empty password (&lt;code&gt;-N ""&lt;/code&gt;) for the CA private key poses a security risk. In production environments, it's strongly recommended to protect the CA private key with a password and store the key offline or in a Hardware Security Module (HSM).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ssh-keygen&lt;/code&gt; output typically includes (output not shown):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Private key: &lt;code&gt;/etc/ssh/ca_key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Public key: &lt;code&gt;/etc/ssh/ca_key.pub&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A secure backup of the root key should be kept offline, such as on a USB drive or in an HSM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing Host Certificates
&lt;/h2&gt;

&lt;p&gt;Each server has a host key (e.g., &lt;code&gt;ssh_host_rsa_key&lt;/code&gt;); this key needs to be certified with the CA's signature.&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;# Sign the host key on the server&lt;/span&gt;
&lt;span class="c"&gt;# Replace ${HOSTNAME} and ${HOSTNAME}.example.com with your server names.&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ssh-keygen &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/ssh/ca_key &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;,&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.example.com /etc/ssh/ssh_host_rsa_key.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates a certificate named &lt;code&gt;ssh_host_rsa_key-cert.pub&lt;/code&gt; in the same directory. Add this certificate file to the &lt;code&gt;HostCertificate&lt;/code&gt; line in the SSH daemon's &lt;code&gt;sshd_config&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In /etc/ssh/sshd_config&lt;/span&gt;
HostKey /etc/ssh/ssh_host_rsa_key
HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt;: Incorrect configurations in &lt;code&gt;sshd_config&lt;/code&gt; can disrupt SSH access. It's recommended to back up the file before making changes and, if possible, perform a dry run or validation step.&lt;/p&gt;

&lt;p&gt;Restart the daemon after saving the changes:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client-Side: Making the CA Public Key Trusted
&lt;/h2&gt;

&lt;p&gt;On client machines, the CA public key is added to &lt;code&gt;~/.ssh/known_hosts&lt;/code&gt; or &lt;code&gt;/etc/ssh/ssh_known_hosts&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="c"&gt;# Add the CA public key (global)&lt;/span&gt;
&lt;span class="c"&gt;# Replace *.example.com with your domain.&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/ssh
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/ssh/ssh_known_hosts &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
@cert-authority *.example.com &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/ssh/ca_key.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# For a personal user (optional, the above global configuration is sufficient)&lt;/span&gt;
&lt;span class="c"&gt;# Replace *.example.com with your domain.&lt;/span&gt;
&lt;span class="c"&gt;# This command adds the CA public key to the user's known_hosts file with the @cert-authority directive.&lt;/span&gt;
&lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; ~/.ssh/known_hosts &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
@cert-authority *.example.com &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/ssh/ca_key.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line accepts all host certificates under &lt;code&gt;*.example.com&lt;/code&gt; signed by the CA.&lt;/p&gt;

&lt;h3&gt;
  
  
  User Certificate (Optional)
&lt;/h3&gt;

&lt;p&gt;If you also want to manage individual user keys, you can create a user certificate with the same CA:&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;# Replace user01 and +52w with your username and validity period.&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/ssh/ca_key &lt;span class="nt"&gt;-I&lt;/span&gt; user01 &lt;span class="nt"&gt;-n&lt;/span&gt; user01 &lt;span class="nt"&gt;-V&lt;/span&gt; +52w ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command produces a file named &lt;code&gt;id_rsa-cert.pub&lt;/code&gt;. On the server, to make this user certificate trusted, you need to specify the CA's public key in the &lt;code&gt;TrustedUserCAKeys&lt;/code&gt; directive in &lt;code&gt;sshd_config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example &lt;code&gt;sshd_config&lt;/code&gt; configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# In /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ca_user_keys.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;/etc/ssh/ca_user_keys.pub&lt;/code&gt; should contain the CA's public key that signed the user certificates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment and Management Strategies
&lt;/h2&gt;

&lt;p&gt;When deploying SSH CA to multiple servers, the following &lt;strong&gt;practical tips&lt;/strong&gt; are useful:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;th&gt;Disadvantage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manual deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copy files to each server using &lt;code&gt;scp&lt;/code&gt;/&lt;code&gt;rsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Simple, low infrastructure need&lt;/td&gt;
&lt;td&gt;High error potential, not scalable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ansible Playbook&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;copy&lt;/code&gt; and &lt;code&gt;lineinfile&lt;/code&gt; modules for automation&lt;/td&gt;
&lt;td&gt;Repeatable, version control&lt;/td&gt;
&lt;td&gt;Requires Ansible setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitOps (Flux/ArgoCD)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Store CA files in a Git repo and deploy via CI/CD&lt;/td&gt;
&lt;td&gt;Change tracking, easy rollback&lt;/td&gt;
&lt;td&gt;Requires CI/CD pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example &lt;strong&gt;Ansible&lt;/strong&gt; task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy SSH CA and host certificates&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy CA public key&lt;/span&gt;
      &lt;span class="na"&gt;copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ca_key.pub&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/ca_key.pub&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure HostCertificate is configured&lt;/span&gt;
      &lt;span class="na"&gt;lineinfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/sshd_config&lt;/span&gt;
        &lt;span class="na"&gt;regexp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^HostCertificate'&lt;/span&gt;
        &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HostCertificate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/etc/ssh/ssh_host_rsa_key-cert.pub"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restart sshd service&lt;/span&gt;
      &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sshd&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restarted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This playbook deploys the CA public key and host certificate in one step.&lt;/p&gt;

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

&lt;p&gt;The CA's private key should &lt;strong&gt;never&lt;/strong&gt; be transferred over the network; it should only be copied with physical access or stored in an HSM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key rotation&lt;/strong&gt;: Regularly rotating (e.g., every 6-12 months) the CA key and re-signing old certificates can enhance long-term security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation&lt;/strong&gt;: OpenSSH uses Key Revocation Lists (KRLs) to revoke keys or certificates. KRLs are binary files created with &lt;code&gt;ssh-keygen -k&lt;/code&gt;. The list of revoked keys (&lt;code&gt;revoked_keys&lt;/code&gt;) is specified in &lt;code&gt;sshd_config&lt;/code&gt; with the &lt;code&gt;RevokedKeys&lt;/code&gt; directive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt;: The &lt;code&gt;ssh-keygen -R&lt;/code&gt; command removes a key from &lt;code&gt;known_hosts&lt;/code&gt;, it does not create a revocation list. Use &lt;code&gt;ssh-keygen -k&lt;/code&gt; for key revocation.&lt;/p&gt;

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

&lt;p&gt;Setting up your own SSH Certificate Authority eliminates key conflicts, centralizes access management, and simplifies revocation processes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1&lt;/strong&gt;: Create the CA root private key in a secure location and protect it with a password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2&lt;/strong&gt;: Sign server host keys with the CA and add the &lt;code&gt;HostCertificate&lt;/code&gt; directive to &lt;code&gt;sshd_config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3&lt;/strong&gt;: Make the CA public key a trusted source on client machines using the &lt;code&gt;@cert-authority&lt;/code&gt; directive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4&lt;/strong&gt;: Scale deployment with automation (Ansible, GitOps).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup enhances both &lt;strong&gt;security&lt;/strong&gt; and &lt;strong&gt;operational efficiency&lt;/strong&gt;; planning for CA rotation and revocation strategies is recommended as a next step.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.rockylinux.org/10/guides/security/ssh_ca_key_signing/" rel="noopener noreferrer"&gt;rockylinux.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cert.kit.edu/ssh/en/OpenSSH/server/" rel="noopener noreferrer"&gt;kit.edu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.venafi.com/Docs/26.1/TopNav/Content/SSH/SSHCertificates/t-ssh-certificate-configure-openssh-trust-venafi-ca.php" rel="noopener noreferrer"&gt;venafi.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/C2SP/C2SP/blob/main/well-known-ssh-hosts.md" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.rundeck.com/docs/learning/howto/revoke-ssh-keys.html" rel="noopener noreferrer"&gt;rundeck.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/deployment_guide/sec-revoking_an_ssh_ca_certificate" rel="noopener noreferrer"&gt;redhat.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man1/ssh-keygen.1.html" rel="noopener noreferrer"&gt;man7.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://goteleport.com/blog/how-to-configure-ssh-certificate-based-authentication/" rel="noopener noreferrer"&gt;goteleport.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>guide</category>
      <category>software</category>
    </item>
    <item>
      <title>Embedding Selection for Turkish: Semantic Search with Multilingual</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:53:01 +0000</pubDate>
      <link>https://dev.to/merbayerp/embedding-selection-for-turkish-semantic-search-with-multilingual-1po2</link>
      <guid>https://dev.to/merbayerp/embedding-selection-for-turkish-semantic-search-with-multilingual-1po2</guid>
      <description>&lt;h2&gt;
  
  
  What is Embedding and Why is it Important?
&lt;/h2&gt;

&lt;p&gt;Embedding is a function that converts text into a high-dimensional vector; sentences with the same meaning are located close to each other in this vector space. In semantic search systems, the similarity between query and document vectors directly determines the quality of the search. Comparing millions of documents in a few milliseconds is only possible with the right embedding selection. In this section, I will summarize the basic logic of embedding and its role in search performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Select Embedding for Turkish in Multilingual Models?
&lt;/h2&gt;

&lt;p&gt;When selecting a multilingual model for Turkish, first, look at the model's &lt;strong&gt;vocabulary&lt;/strong&gt; and &lt;strong&gt;pre-training&lt;/strong&gt; dataset's representation of Turkish. If the model tokenizes Turkish characters (ç, ğ, ı, ö, ş, ü) correctly, it produces consistent vectors for the same word in different forms (e.g., "kredi" vs "krediği"). Using the &lt;code&gt;sentence-transformers&lt;/code&gt; package from HuggingFace, you can download and test a model with just a few lines of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="n"&gt;model_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Müşteri siparişini onayladı.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Müşteri siparişini onayladı.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (2, 384)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet generates 384-dimensional vectors for two identical sentences and, with &lt;code&gt;normalize_embeddings=True&lt;/code&gt;, the cosine similarity directly approaches 1.0. In a real environment, running different models with the same test set and comparing &lt;strong&gt;cosine similarity&lt;/strong&gt; values is the primary criterion for the selection process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact of Language Features and Tokenization
&lt;/h2&gt;

&lt;p&gt;Turkish, being an agglutinative language, has long word forms; tokenization errors can negate this. For example, BPE-based tokenizers split the word "çalıştırma" into multiple tokens, while a word-based tokenizer might keep it as a single token. This difference can produce vectors of different lengths and, consequently, different similarity scores. When selecting a model, consider how the tokenizer splits words into subword units like &lt;strong&gt;sentence-piece&lt;/strong&gt; or &lt;strong&gt;WordPiece&lt;/strong&gt;, especially for long sentences, to increase consistency.&lt;/p&gt;

&lt;p&gt;The example below compares the same sentence tokenized by two different tokenizers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from transformers import AutoTokenizer;
t1 = AutoTokenizer.from_pretrained('bert-base-multilingual-cased');
t2 = AutoTokenizer.from_pretrained('xlm-roberta-base'); # Using XLM-R to demonstrate different tokenization behavior
print('BERT tokens:', t1.tokenize('çalışma ortamı güvenli mi?'));
print('XLM-R tokens:', t2.tokenize('çalışma ortamı güvenli mi?'))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that BERT splits the sentence into 6 parts using &lt;strong&gt;WordPiece&lt;/strong&gt;-based tokenization, while XLM-R splits it into 5 parts. This difference can affect the sentence's position in the vector space, particularly critical in &lt;strong&gt;retrieval-augmented generation (RAG)&lt;/strong&gt; scenarios where correct tokenization is key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Comparison: mBERT vs XLM-R vs LaBSE vs Sentence-Transformer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size (Parameters)&lt;/th&gt;
&lt;th&gt;Output Dimension&lt;/th&gt;
&lt;th&gt;Turkish Pre-training Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bert-base-multilingual-cased&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;110M&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;104-language Wikipedia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xlm-roberta-base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~270M&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;100+ language CommonCrawl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LaBSE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~800M&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;Texts in 109 languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paraphrase-multilingual-MiniLM-L12-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~22M&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;Multilingual texts (50+ languages)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The parameter counts in the table are approximate and based on information from model cards or related publications. In a production environment, a lighter model like &lt;strong&gt;MiniLM&lt;/strong&gt; offers a balance between low latency and sufficient accuracy. This decision is made based on the need to run on &lt;strong&gt;CPU-only&lt;/strong&gt; servers or &lt;strong&gt;edge&lt;/strong&gt; devices.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Model Selection: Memory and Latency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Smaller models typically require less RAM, while larger models demand more. For instance, some MiniLM models can use under 50 MB of memory for inference, while larger models may require hundreds of MB or even GB of memory. Memory usage depends on the model size, precision used (FP32, FP16, INT8), and batch size. Measure latency using &lt;code&gt;time python script.py&lt;/code&gt; and compare results directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Performance Measurement and Evaluation Methodology
&lt;/h2&gt;

&lt;p&gt;When measuring performance, two primary metrics are used: &lt;strong&gt;Recall@k&lt;/strong&gt; and &lt;strong&gt;Mean Reciprocal Rank (MRR)&lt;/strong&gt;. Recall@k gives the rate of finding the correct document within the first &lt;em&gt;k&lt;/em&gt; results; MRR provides an average score by taking the reciprocal of the correct document's rank. A sample evaluation pipeline includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Preparing a suitable-sized Turkish query-document test set.&lt;/li&gt;
&lt;li&gt;Generating model embeddings for each query and searching for the corresponding document in the FAISS index.&lt;/li&gt;
&lt;li&gt;Retrieving the top &lt;em&gt;k&lt;/em&gt; results and comparing them with the ground truth.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Below is a simple evaluation code using &lt;code&gt;faiss&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;          &lt;span class="c1"&gt;# Query sentences
&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;             &lt;span class="c1"&gt;# Document sentences
&lt;/span&gt;&lt;span class="n"&gt;doc_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IndexFlatIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&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="c1"&gt;# IndexFlatIP is used for inner product similarity
&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;true_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
        &lt;span class="n"&gt;q_emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;true_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recall@10:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script prints the &lt;strong&gt;Recall@10&lt;/strong&gt; value directly to the terminal; saving the output and comparing it with different models makes the decision process objective. In a real project, adding &lt;strong&gt;GPU&lt;/strong&gt; acceleration and reporting measurement time can further enhance the evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Environment: Embedding Service and Query Optimization
&lt;/h2&gt;

&lt;p&gt;In a production environment, serving the embedding calculation via a &lt;strong&gt;REST&lt;/strong&gt; or &lt;strong&gt;gRPC&lt;/strong&gt; API provides a scalable solution. The following Docker-Compose example runs a &lt;code&gt;sentence-transformers&lt;/code&gt; service with &lt;code&gt;uvicorn&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="s2"&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;embed-api&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;python:3.11-slim&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&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;./:/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;sh -c "pip install sentence-transformers fastapi uvicorn &amp;amp;&amp;amp;&lt;/span&gt;
             &lt;span class="s"&gt;uvicorn embed_api:app --host 0.0.0.0 --port 8000" # Uvicorn command line arguments&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;8000:8000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# embed_api.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Creating a FastAPI application
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# Using Pydantic BaseModel
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/embed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This service can handle &lt;strong&gt;batch&lt;/strong&gt; requests to process multiple queries simultaneously; monitoring &lt;strong&gt;CPU&lt;/strong&gt; usage with &lt;code&gt;top&lt;/code&gt; or &lt;code&gt;htop&lt;/code&gt; is crucial to determine resource limits. For query optimization, configuring an index like &lt;strong&gt;FAISS IVF+PQ&lt;/strong&gt; provides millisecond response times for large collections. Additionally, adding a &lt;strong&gt;caching&lt;/strong&gt; layer (e.g., Redis) to store embeddings for frequently queried texts can significantly reduce latency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/embed &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text":"How does semantic search work in Turkish texts?"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command returns an embedding in response to a single HTTP request; measuring the response time with &lt;code&gt;time curl ...&lt;/code&gt; can verify performance targets.&lt;/p&gt;

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

&lt;p&gt;Selecting an embedding for Turkish text depends on the model's representation of Turkish in its pre-training dataset, tokenizer compatibility, and resource constraints in the production environment. &lt;strong&gt;MiniLM&lt;/strong&gt;-like lightweight models offer a balance between low latency and sufficient accuracy, while larger models like &lt;strong&gt;LaBSE&lt;/strong&gt; provide higher recall but require more memory and GPU resources. By measuring &lt;strong&gt;Recall@k&lt;/strong&gt; and &lt;strong&gt;MRR&lt;/strong&gt; with a real test set, you can objectively determine the most suitable model for your needs. The next step involves integrating the selected model into a CI/CD pipeline and monitoring its performance continuously with tools like Prometheus and Grafana.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/huggingface/sentence-transformers/blob/main/docs/sentence_transformer/usage/usage.rst" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.langchain.com/oss/python/integrations/embeddings/sentence_transformers" rel="noopener noreferrer"&gt;langchain.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Kludex/uvicorn/blob/main/docs/settings.md" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facebookresearch/faiss/wiki/Faiss-indexes" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/python/introduction-to-fastapi/" rel="noopener noreferrer"&gt;geeksforgeeks.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" rel="noopener noreferrer"&gt;huggingface.co&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/onnx-models/paraphrase-multilingual-MiniLM-L12-v2-onnx" rel="noopener noreferrer"&gt;huggingface.co&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/04/paraphrase-multilingual-table-transformer-bielik-on-sagemaker-jumpstart/" rel="noopener noreferrer"&gt;amazon.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>sistemmimarisi</category>
      <category>software</category>
    </item>
    <item>
      <title>Transcribing Turkish with Whisper: Local Audio‑Text Pipeline</title>
      <dc:creator>Mustafa ERBAY</dc:creator>
      <pubDate>Fri, 14 Aug 2026 02:36:17 +0000</pubDate>
      <link>https://dev.to/merbayerp/transcribing-turkish-with-whisper-local-audio-text-pipeline-2kd1</link>
      <guid>https://dev.to/merbayerp/transcribing-turkish-with-whisper-local-audio-text-pipeline-2kd1</guid>
      <description>&lt;h2&gt;
  
  
  Whisper model and Turkish support
&lt;/h2&gt;

&lt;p&gt;The Whisper model is an open‑source, multilingual speech‑to‑text system released by OpenAI in September 2022. Because it supports 99 languages, it can be used out of the box without any language‑specific fine‑tuning, and it includes Turkish. The &lt;code&gt;large&lt;/code&gt; variant has 1.55 billion parameters.&lt;/p&gt;

&lt;p&gt;Whisper’s architecture uses an encoder‑decoder Transformer. This design provides robustness to various accents and background noise, allowing it to turn recordings from low‑quality microphones into meaningful text. The training dataset consists of 680 k hours of multilingual, multitask supervised audio collected from the web.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ℹ️ Audio quality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model performs best with mono audio files sampled at 16 kHz. Lower sampling rates or stereo audio can affect transcription accuracy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Environment setup and dependency management
&lt;/h2&gt;

&lt;p&gt;To use Whisper you need Python 3.8–3.11 and the &lt;code&gt;pip&lt;/code&gt; package manager. First make sure the &lt;code&gt;ffmpeg&lt;/code&gt; tool is installed on your system, as Whisper relies on &lt;code&gt;ffmpeg&lt;/code&gt; for audio preprocessing.&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;# Ubuntu or Debian based systems&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;span class="c"&gt;# macOS (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;span class="c"&gt;# Windows (Chocolatey)&lt;/span&gt;
choco &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the Whisper package with &lt;code&gt;pip&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;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; openai-whisper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have a CUDA‑capable GPU, installing a compatible PyTorch build for GPU‑accelerated inference is recommended. For example, for CUDA 11.8 you can use the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;torch torchvision torchaudio &lt;span class="nt"&gt;--index-url&lt;/span&gt; https://download.pytorch.org/whl/cu118
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no GPU is available, use the following command to install a CPU‑optimized PyTorch build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;torch torchvision torchaudio &lt;span class="nt"&gt;--index-url&lt;/span&gt; https://download.pytorch.org/whl/cpu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A virtual environment (venv) provides an isolated Python environment; creating one with &lt;code&gt;python -m venv .venv &amp;amp;&amp;amp; source .venv/bin/activate&lt;/code&gt; runs independently of system packages and prevents dependency conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing audio files and preprocessing
&lt;/h2&gt;

&lt;p&gt;Although Whisper supports many audio formats, it is recommended that files be in WAV or FLAC format with a 16 kHz sampling rate and a single channel (mono). You can perform this conversion with &lt;code&gt;ffmpeg&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;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp3 &lt;span class="nt"&gt;-ar&lt;/span&gt; 16000 &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalizing the volume of recordings captured in noisy environments can be done as an optional preprocessing step with external tools like &lt;code&gt;sox&lt;/code&gt;. For example, the command &lt;code&gt;sox output.wav normalized.wav gain -n&lt;/code&gt; can normalize the average RMS level. This helps reduce the risk of the model missing words in low‑volume sections.&lt;/p&gt;

&lt;p&gt;Whisper processes audio internally in 30‑second chunks. For very long files (e.g., several hours) or systems with limited memory, splitting the file into smaller segments can help manage memory usage. For instance, you can split a file into 10‑minute pieces using &lt;code&gt;ffmpeg&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;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; normalized.wav &lt;span class="nt"&gt;-f&lt;/span&gt; segment &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 600 &lt;span class="nt"&gt;-c&lt;/span&gt; copy part_%03d.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Transcription steps with Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;whisper&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="c1"&gt;# Load the large model variant.
# Model weights are typically downloaded to ~/.cache/whisper.
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;whisper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;audio_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;part_000.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# A pre‑processed chunk
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snippet above shows that &lt;code&gt;load_model&lt;/code&gt; downloads the model weights, and the &lt;code&gt;transcribe&lt;/code&gt; method, with &lt;code&gt;language="tr"&lt;/code&gt; specified, disables automatic language detection and locks it to Turkish. The result dictionary contains the raw transcript under the &lt;code&gt;text&lt;/code&gt; key and timestamps in the &lt;code&gt;segments&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;Each entry in the &lt;code&gt;segments&lt;/code&gt; list is structured like &lt;code&gt;{"id":0, "seek":0, "start":0.0, "end":5.12, "text":"Hello world", "tokens":[...]}&lt;/code&gt;, providing a clear view of which words were spoken during a specific time interval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Combine multiple segments from a single file
&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating a loop for multiple audio parts can be done easily with &lt;code&gt;for part in pathlib.Path('.').glob('part_*.wav'):&lt;/code&gt;; this aggregates a large collection of audio files into a single transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post‑processing: text cleaning and timestamps
&lt;/h2&gt;

&lt;p&gt;The transcription output usually includes punctuation and case conversion, but additional text cleaning and formatting can improve the end‑user experience. For example, you can replace double spaces with a single space and capitalize the first character of a sentence using: &lt;code&gt;result["text"].replace("  ", " ").strip().capitalize()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Timestamps are available as &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; fields in &lt;code&gt;result["segments"]&lt;/code&gt;. Converting them to SRT format can be done with a simple loop using the &lt;code&gt;srt&lt;/code&gt; library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;srt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_srt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segments&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="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;srt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Subtitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;srt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                             &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;srt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                             &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;srt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;srt_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;to_srt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.srt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srt_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code saves each segment as a subtitle line, enabling media players to synchronize it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance monitoring and debugging
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;torch.cuda.is_available()&lt;/code&gt; checks whether a GPU is present. If it returns &lt;code&gt;True&lt;/code&gt;, you can create a GPU‑enabled model with &lt;code&gt;model = whisper.load_model("large", device="cuda")&lt;/code&gt;. When no GPU is available, the &lt;code&gt;device="cpu"&lt;/code&gt; option automatically runs a CPU‑optimized execution.&lt;/p&gt;

&lt;p&gt;To measure processing time you can use the &lt;code&gt;time&lt;/code&gt; module: &lt;code&gt;start = time.time(); model.transcribe(...); elapsed = time.time() - start&lt;/code&gt;. This quantifies performance differences across audio lengths and model variants.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Memory constraints&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a CPU‑only environment the &lt;code&gt;large&lt;/code&gt; model can consume a substantial amount of RAM (roughly 3.9 GB up to 10 GB). If memory is insufficient, you may need to switch to smaller model variants such as &lt;code&gt;medium&lt;/code&gt; or &lt;code&gt;small&lt;/code&gt; to avoid out‑of‑memory errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pipeline visualization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJBdWRpbyBGaWxlIl0gLS0-IEJbIlByZeKAkXByb2Nlc3NpbmcgKGZmbXBlZykiXTsKICAgIEIgLS0-IENbIldoaXNwZXIgTW9kZWwgKGxhcmdlKSJdOwogICAgQyAtLT4gRFsiVHJhbnNjcmlwdGlvbiAoc2VnbWVudHMpIl07CiAgICBEIC0tPiBFWyJQb3N04oCRUHJvY2Vzc2luZyAoY2xlYW5pbmcsIFNSVCkiXTsKICAgIEUgLS0-IEZbIlJlc3VsdCAodHh0IC8gc3J0KSJdOw%3Ftype%3Dpng%26bgColor%3Dwhite" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQ7CiAgICBBWyJBdWRpbyBGaWxlIl0gLS0-IEJbIlByZeKAkXByb2Nlc3NpbmcgKGZmbXBlZykiXTsKICAgIEIgLS0-IENbIldoaXNwZXIgTW9kZWwgKGxhcmdlKSJdOwogICAgQyAtLT4gRFsiVHJhbnNjcmlwdGlvbiAoc2VnbWVudHMpIl07CiAgICBEIC0tPiBFWyJQb3N04oCRUHJvY2Vzc2luZyAoY2xlYW5pbmcsIFNSVCkiXTsKICAgIEUgLS0-IEZbIlJlc3VsdCAodHh0IC8gc3J0KSJdOw%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Diagram" width="276" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This diagram shows the flow from an audio file to the final text as a single‑direction pipeline; each box takes the output of the previous step and feeds it into the next.&lt;/p&gt;

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

&lt;p&gt;Processing Turkish audio files with Whisper locally makes it possible to generate high‑accuracy transcripts when proper preprocessing and model selection are applied. Clear command and configuration examples at each pipeline stage create a reusable template and enable rapid integration into production environments.&lt;/p&gt;

&lt;p&gt;The next step is to add this pipeline to a CI/CD workflow to provide an automated transcription service; an example GitHub Actions file runs the &lt;code&gt;whisper&lt;/code&gt; command inside a Docker container, offering a scalable solution.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/whisper" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/whisper/discussions/5" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.gdeltproject.org/a-deep-dive-exploration-applying-openais-whisper-asr-to-a-russian-television-news-broadcast/" rel="noopener noreferrer"&gt;gdeltproject.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gladia.io/blog/what-is-openai-whisper" rel="noopener noreferrer"&gt;gladia.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.whispertranscribe.com/blog/how-to-transcribe-audio-file-using-whisper" rel="noopener noreferrer"&gt;whispertranscribe.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sally.io/blog/openai-whisper-features" rel="noopener noreferrer"&gt;sally.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zilliz.com/glossary/openai-whisper" rel="noopener noreferrer"&gt;zilliz.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whisper-api.com/docs/languages/" rel="noopener noreferrer"&gt;whisper-api.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>guide</category>
      <category>software</category>
    </item>
  </channel>
</rss>
