<?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: Ramansah</title>
    <description>The latest articles on DEV Community by Ramansah (@ramansah).</description>
    <link>https://dev.to/ramansah</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%2F4006638%2Fb5a25202-3633-46b8-8f8c-00c38bfa8b37.png</url>
      <title>DEV Community: Ramansah</title>
      <link>https://dev.to/ramansah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ramansah"/>
    <language>en</language>
    <item>
      <title>How to Set Up Prometheus and Grafana Monitoring Stack with Docker Compose</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Mon, 13 Jul 2026 09:57:00 +0000</pubDate>
      <link>https://dev.to/ramansah/how-to-set-up-prometheus-and-grafana-monitoring-stack-with-docker-compose-3ea6</link>
      <guid>https://dev.to/ramansah/how-to-set-up-prometheus-and-grafana-monitoring-stack-with-docker-compose-3ea6</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What You're Building&lt;/li&gt;
&lt;li&gt;How Prometheus and Grafana Work Together&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Project Structure&lt;/li&gt;
&lt;li&gt;Step 1: Prometheus Configuration&lt;/li&gt;
&lt;li&gt;Step 2: Alert Rules&lt;/li&gt;
&lt;li&gt;Step 3: Alertmanager Configuration&lt;/li&gt;
&lt;li&gt;Step 4: Grafana Auto-Provisioning&lt;/li&gt;
&lt;li&gt;Step 5: The Full Docker Compose Stack&lt;/li&gt;
&lt;li&gt;Step 6: Start the Stack and Verify&lt;/li&gt;
&lt;li&gt;Step 7: Importing Your First Dashboard&lt;/li&gt;
&lt;li&gt;Step 8: Writing Your First PromQL Query&lt;/li&gt;
&lt;li&gt;Monitoring Docker Containers with cAdvisor&lt;/li&gt;
&lt;li&gt;Production Hardening Checklist&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Next Steps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most infrastructure problems don't announce themselves — they build up quietly until something breaks. A server runs out of disk space at 3 AM. A container silently restarts 50 times in an hour. Memory creeps toward its limit over three days before everything grinds to a halt. The difference between catching these early and waking up to an outage is a monitoring stack that's actually watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prometheus&lt;/strong&gt; collects and stores metrics from your infrastructure. &lt;strong&gt;Grafana&lt;/strong&gt; turns those metrics into dashboards and alerts you can actually act on. Together with &lt;strong&gt;Node Exporter&lt;/strong&gt; (host metrics) and &lt;strong&gt;Alertmanager&lt;/strong&gt; (notifications), they form the standard open-source observability stack used by teams ranging from small self-hosted setups to large-scale production environments.&lt;/p&gt;

&lt;p&gt;This guide builds the full stack with Docker Compose — Prometheus, Grafana, Node Exporter, cAdvisor, and Alertmanager — with auto-provisioning, persistent storage, and a production hardening checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You're Building
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; scraping metrics every 15 seconds from all services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node Exporter&lt;/strong&gt; exposing host-level metrics (CPU, memory, disk, network)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cAdvisor&lt;/strong&gt; exposing per-container Docker metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; with Prometheus auto-configured as a data source and pre-loaded dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager&lt;/strong&gt; sending notifications when something goes wrong&lt;/li&gt;
&lt;li&gt;Everything persisted in named volumes and isolated on a dedicated Docker network&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Prometheus and Grafana Work Together
&lt;/h2&gt;

&lt;p&gt;Understanding the data flow makes troubleshooting much easier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host OS / Docker containers
        │
        │ expose metrics endpoints
        ▼
Node Exporter (:9100) + cAdvisor (:8080)
        │
        │ Prometheus pulls (scrapes) every 15s
        ▼
Prometheus (:9090) ──── stores TSDB on disk
        │
        │ Grafana queries via PromQL
        ▼
Grafana (:3000) ──── dashboards + alerts
        │
        │ fires alert rules → Alertmanager
        ▼
Alertmanager (:9093) ──── sends to Slack/email/PagerDuty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical concept: &lt;strong&gt;Prometheus is pull-based&lt;/strong&gt;, not push-based. Prometheus reaches out to each target and scrapes its &lt;code&gt;/metrics&lt;/code&gt; endpoint on a schedule. Services don't push data to Prometheus — they expose an HTTP endpoint and wait for Prometheus to come collect. This pull model makes it easy to add or remove monitoring targets without touching the monitored service.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Docker and Docker Compose installed (see our &lt;a href="https://bckinfo.com/install-docker-docker-compose-ubuntu-26-04-lts/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu 26.04 LTS&lt;/a&gt; guide)&lt;/li&gt;
&lt;li&gt;At least 2GB of free RAM (Prometheus TSDB is memory-hungry under load)&lt;/li&gt;
&lt;li&gt;A server or VM you want to monitor&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monitoring-stack/
├── compose.yml
├── .env
├── prometheus/
│   ├── prometheus.yml
│   └── alert.rules.yml
├── alertmanager/
│   └── alertmanager.yml
└── grafana/
    └── provisioning/
        ├── datasources/
        │   └── datasource.yml
        └── dashboards/
            └── dashboards.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/monitoring-stack/&lt;span class="o"&gt;{&lt;/span&gt;prometheus,alertmanager,grafana/provisioning/&lt;span class="o"&gt;{&lt;/span&gt;datasources,dashboards&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/monitoring-stack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Prometheus Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus/prometheus.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;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;external_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;monitor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bckinfo-monitoring'&lt;/span&gt;

&lt;span class="na"&gt;alerting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;alertmanagers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;static_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;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;alertmanager:9093&lt;/span&gt;

&lt;span class="na"&gt;rule_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/prometheus/alert.rules.yml&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Prometheus self-monitoring&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prometheus'&lt;/span&gt;
    &lt;span class="na"&gt;static_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;targets&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;prometheus:9090'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Host metrics via Node Exporter&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&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-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;static_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;targets&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-exporter:9100'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Docker container metrics via cAdvisor&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cadvisor'&lt;/span&gt;
    &lt;span class="na"&gt;static_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;targets&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;cadvisor:8080'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;job_name&lt;/code&gt; in &lt;code&gt;scrape_configs&lt;/code&gt; corresponds to a service in the Compose stack. The hostnames (like &lt;code&gt;node-exporter&lt;/code&gt;, &lt;code&gt;cadvisor&lt;/code&gt;) are Docker service names resolved over the shared monitoring network — the same hostname-as-service-name pattern used in our &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;Redis with Docker Compose&lt;/a&gt; and &lt;a href="https://bckinfo.com/mongodb-docker-compose-replica-set-authentication/" rel="noopener noreferrer"&gt;MongoDB with Docker Compose&lt;/a&gt; guides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Alert Rules
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus/alert.rules.yml&lt;/span&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;host-alerts&lt;/span&gt;
    &lt;span class="na"&gt;interval&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;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Alert when any scrape target is down&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;TargetDown&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;up == &lt;/span&gt;&lt;span class="m"&gt;0&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;2m&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;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;Target&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.job&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;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;down"&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;{{&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;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;unreachable&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;more&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;than&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;minutes."&lt;/span&gt;

      &lt;span class="c1"&gt;# Alert when CPU usage &amp;gt; 85% for 5 minutes&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;HighCpuUsage&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;85&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;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;High&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CPU&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&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;{{&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="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;CPU&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&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;above&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;85%&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;more&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;than&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;%.1f&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;span class="c1"&gt;# Alert when memory usage &amp;gt; 90%&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;HighMemoryUsage&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;90&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;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;High&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&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;{{&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="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;Memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&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;above&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;90%.&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;%.1f&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;span class="c1"&gt;# Alert when disk usage &amp;gt; 85%&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;DiskSpaceRunningLow&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;(1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs"} / node_filesystem_size_bytes)) * 100 &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;85&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;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;Disk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;space&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;low&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;{{&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="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;Filesystem&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.mountpoint&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;is&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;%.1f&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;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;full."&lt;/span&gt;

      &lt;span class="c1"&gt;# Alert when disk will be full in less than 24 hours&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;DiskWillFillIn24Hours&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs"}[6h], 24 * 3600) &amp;lt; &lt;/span&gt;&lt;span class="m"&gt;0&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;30m&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;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;Disk&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;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;24&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;on&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="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;Filesystem&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.mountpoint&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;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;predicted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&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;24&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hours."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;predict_linear&lt;/code&gt; in the last rule is one of Prometheus's most useful functions — it projects the current trajectory of a metric forward in time, letting you alert on trends before they become crises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Alertmanager Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# alertmanager/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;slack_api_url&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_WEBHOOK_URL}'&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;instance'&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;slack-notifications'&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;slack-notifications'&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;1h&lt;/span&gt;

&lt;span class="na"&gt;receivers&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slack-notifications'&lt;/span&gt;
    &lt;span class="na"&gt;slack_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;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#alerts'&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&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;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;eq&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"firing"&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;FIRING{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;else&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;RESOLVED{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;end&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;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.GroupLabels.alertname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;text&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;{{ range .Alerts }}&lt;/span&gt;
          &lt;span class="s"&gt;*Alert:* {{ .Annotations.summary }}&lt;/span&gt;
          &lt;span class="s"&gt;*Description:* {{ .Annotations.description }}&lt;/span&gt;
          &lt;span class="s"&gt;*Severity:* {{ .Labels.severity }}&lt;/span&gt;
          &lt;span class="s"&gt;{{ end }}&lt;/span&gt;
        &lt;span class="na"&gt;send_resolved&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;inhibit_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;source_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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;critical'&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;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;warning'&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;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;instance'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;inhibit_rules&lt;/code&gt; section prevents alert storms — if a critical alert fires for an instance, Prometheus suppresses the warning-level alerts for that same instance. This keeps your Slack channel from flooding with redundant notifications when a server goes down.&lt;/p&gt;

&lt;p&gt;If you don't use Slack, Alertmanager also supports email, PagerDuty, OpsGenie, and webhooks — swap the &lt;code&gt;receivers&lt;/code&gt; section accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Grafana Auto-Provisioning
&lt;/h2&gt;

&lt;p&gt;Instead of manually adding Prometheus as a data source in the Grafana UI every time you deploy, use provisioning files to configure it automatically at startup.&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;# grafana/provisioning/datasources/datasource.yml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;datasources&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;Prometheus&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
    &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxy&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://prometheus:9090&lt;/span&gt;
    &lt;span class="na"&gt;isDefault&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;editable&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# grafana/provisioning/dashboards/dashboards.yml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;providers&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default'&lt;/span&gt;
    &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;folder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
    &lt;span class="na"&gt;disableDeletion&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;updateIntervalSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;allowUiUpdates&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;options&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/grafana/dashboards&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same one-shot initialization approach used for MongoDB replica sets and Apache Superset — configuration that runs once at startup, making the deployment fully repeatable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The Full Docker Compose Stack
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; file first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=change_this_strong_password
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# compose.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;monitoring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana-data&lt;/span&gt;&lt;span class="pi"&gt;:&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;prometheus&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;prom/prometheus:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&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;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--config.file=/etc/prometheus/prometheus.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.path=/prometheus'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.retention.time=30d'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.enable-lifecycle'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.enable-admin-api'&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;./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus/alert.rules.yml:/etc/prometheus/alert.rules.yml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus-data:/prometheus&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;127.0.0.1:9090:9090"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-qO-"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9090/-/ready"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&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;5&lt;/span&gt;

  &lt;span class="na"&gt;grafana&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;grafana/grafana:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_USERS_ALLOW_SIGN_UP=false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SERVER_DOMAIN=localhost&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SMTP_ENABLED=false&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;grafana-data:/var/lib/grafana&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./grafana/provisioning:/etc/grafana/provisioning:ro&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;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;node-exporter&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;prom/node-exporter:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&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;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--path.procfs=/host/proc'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--path.sysfs=/host/sys'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--path.rootfs=/rootfs'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'&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;/proc:/host/proc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/sys:/host/sys:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/:/rootfs:ro&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;127.0.0.1:9100:9100"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;

  &lt;span class="na"&gt;cadvisor&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;gcr.io/cadvisor/cadvisor:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cadvisor&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;privileged&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;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;/:/rootfs:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run:/var/run:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/sys:/sys:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/docker/:/var/lib/docker:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/dev/disk/:/dev/disk:ro&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;127.0.0.1:8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;

  &lt;span class="na"&gt;alertmanager&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;prom/alertmanager:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alertmanager&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&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;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--config.file=/etc/alertmanager/alertmanager.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.path=/alertmanager'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL}&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;./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro&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;127.0.0.1:9093:9093"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two security decisions in this Compose file worth calling out explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port bindings use &lt;code&gt;127.0.0.1:&lt;/code&gt;&lt;/strong&gt; for Prometheus, Node Exporter, cAdvisor, and Alertmanager. This means those services are only reachable from the server itself — not exposed to the network. Only Grafana (port 3000) is accessible externally, since that's the only interface end users need. This follows the same network isolation principle covered in our &lt;a href="https://bckinfo.com/docker-container-security-best-practices/" rel="noopener noreferrer"&gt;Docker Container Security Best Practices&lt;/a&gt; guide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GF_USERS_ALLOW_SIGN_UP=false&lt;/code&gt;&lt;/strong&gt; — prevents anyone from self-registering an account on your Grafana instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Start the Stack and Verify
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;Watch services come up:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Verify each service is reachable:&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;# Prometheus ready check&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:9090/-/ready
&lt;span class="c"&gt;# Expected: "Prometheus Server is Ready."&lt;/span&gt;

&lt;span class="c"&gt;# Node Exporter metrics check&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:9100/metrics | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Grafana health check&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3000/api/health
&lt;span class="c"&gt;# Expected: {"commit":"...","database":"ok","version":"..."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;:3000&lt;/code&gt; in a browser and log in with the credentials from your &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Importing Your First Dashboard
&lt;/h2&gt;

&lt;p&gt;Grafana has thousands of community-built dashboards at &lt;a href="https://grafana.com/grafana/dashboards" rel="noopener noreferrer"&gt;grafana.com/grafana/dashboards&lt;/a&gt;. The most useful ones for a server monitoring stack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node Exporter Full — Dashboard ID: 1860&lt;/strong&gt;&lt;br&gt;
The gold standard for host metrics — CPU, memory, disk I/O, network, load average, all in one view.&lt;/p&gt;

&lt;p&gt;To import: &lt;strong&gt;Dashboards → Import → Enter ID &lt;code&gt;1860&lt;/code&gt; → Load → select your Prometheus data source → Import&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker and Container Metrics — Dashboard ID: 193&lt;/strong&gt;&lt;br&gt;
Container CPU, memory, and network stats from cAdvisor, with per-container breakdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prometheus 2.0 Stats — Dashboard ID: 3662&lt;/strong&gt;&lt;br&gt;
Self-monitoring for Prometheus itself — scrape durations, TSDB metrics, sample ingestion rate.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 8: Writing Your First PromQL Query
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prometheus Query Language (PromQL)&lt;/strong&gt; is what Grafana uses to pull data from Prometheus. A few practical queries to get started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Current CPU usage percentage (all cores, 5-minute average)
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# Available memory in GB
node_memory_MemAvailable_bytes / 1024 / 1024 / 1024

# Disk usage percentage per filesystem
(1 - (node_filesystem_avail_bytes / node_filesystem_size_bytes)) * 100

# Number of running Docker containers
count(container_last_seen{image!=""})

# Container CPU usage rate (specific container by name)
rate(container_cpu_usage_seconds_total{name="redis"}[5m]) * 100

# HTTP request rate per second
rate(prometheus_http_requests_total[5m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Grafana, go to &lt;strong&gt;Explore&lt;/strong&gt; (compass icon in the left sidebar), select your Prometheus data source, paste any query above into the metrics input, and click &lt;strong&gt;Run Query&lt;/strong&gt; to see results immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Docker Containers with cAdvisor
&lt;/h2&gt;

&lt;p&gt;cAdvisor (Container Advisor) automatically discovers all running containers on the Docker host and exposes their resource usage as Prometheus metrics — no per-container configuration required.&lt;/p&gt;

&lt;p&gt;The metrics cAdvisor provides include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_cpu_usage_seconds_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cumulative CPU time consumed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_usage_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current memory usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_memory_limit_bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Memory limit set on container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_network_transmit_bytes_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network bytes sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_network_receive_bytes_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network bytes received&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;container_fs_reads_bytes_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filesystem read bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For containers running Redis or MongoDB (see our &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; and &lt;a href="https://bckinfo.com/mongodb-docker-compose-replica-set-authentication/" rel="noopener noreferrer"&gt;MongoDB&lt;/a&gt; guides), cAdvisor gives you memory and CPU visibility without any instrumentation changes to those containers — you can immediately see if Redis is approaching its &lt;code&gt;maxmemory&lt;/code&gt; limit or if a MongoDB container is consuming unexpectedly high CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Hardening Checklist
&lt;/h2&gt;

&lt;p&gt;Before this stack serves real infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put Grafana behind a reverse proxy with TLS.&lt;/strong&gt; Port 3000 over plain HTTP is fine for a local lab, not for anything reachable from the internet. Nginx or Traefik with Let's Encrypt handles termination cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change the default Grafana admin password&lt;/strong&gt; from what's in &lt;code&gt;.env&lt;/code&gt; — and rotate it from the CLI, not the UI, on first login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a Prometheus retention period&lt;/strong&gt; matching your storage capacity. &lt;code&gt;--storage.tsdb.retention.time=30d&lt;/code&gt; is a reasonable default; 90 days gives you better trend visibility but needs more disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add resource limits to the Compose file&lt;/strong&gt; — Prometheus and Grafana can consume significant memory under load. Cap them with &lt;code&gt;deploy.resources.limits&lt;/code&gt; to prevent a noisy monitoring stack from affecting the services it's monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up the Grafana volume&lt;/strong&gt; — this is where your dashboards, saved queries, and alert configurations live. A volume backup on a schedule (same approach as the &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;Redis backup scripts&lt;/a&gt; guide) prevents losing custom dashboards when the host is rebuilt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict Prometheus and Alertmanager access&lt;/strong&gt; — in this guide both are already bound to &lt;code&gt;127.0.0.1&lt;/code&gt;. Keep it that way. If you need remote access, use an SSH tunnel or VPN rather than opening those ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus shows target as &lt;code&gt;DOWN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Node Exporter/cAdvisor not reachable on monitoring network&lt;/td&gt;
&lt;td&gt;Confirm all services are on the same &lt;code&gt;monitoring&lt;/code&gt; network; check &lt;code&gt;docker compose ps&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No data in Grafana dashboards&lt;/td&gt;
&lt;td&gt;Wrong data source URL or Prometheus not ready&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;http://prometheus:9090&lt;/code&gt; is set in the data source, not &lt;code&gt;http://localhost:9090&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cAdvisor container fails to start&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;privileged: true&lt;/code&gt; or host volume permissions&lt;/td&gt;
&lt;td&gt;Ensure &lt;code&gt;privileged: true&lt;/code&gt; is set in the cAdvisor service definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alerts firing continuously without resolution&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;resolve_timeout&lt;/code&gt; too short, or condition never clears&lt;/td&gt;
&lt;td&gt;Increase &lt;code&gt;resolve_timeout&lt;/code&gt; in Alertmanager; verify the alert condition logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus disk usage grows too fast&lt;/td&gt;
&lt;td&gt;Retention period too long, or too many high-cardinality labels&lt;/td&gt;
&lt;td&gt;Lower &lt;code&gt;--storage.tsdb.retention.time&lt;/code&gt;, audit your scrape configs for label explosion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana login shows "invalid username or password"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; not loaded, or password has special characters breaking YAML&lt;/td&gt;
&lt;td&gt;Wrap the password in quotes in &lt;code&gt;.env&lt;/code&gt;; verify &lt;code&gt;docker compose config&lt;/code&gt; shows the correct values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;With Prometheus and Grafana running, you can extend the stack by adding exporters for the specific services in your infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Exporter&lt;/strong&gt; (&lt;code&gt;oliver006/redis_exporter&lt;/code&gt;) — see this alongside our &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;Redis with Docker Compose&lt;/a&gt; guide for memory usage, keyspace hits, and eviction rates per Redis instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB Exporter&lt;/strong&gt; (&lt;code&gt;percona/mongodb_exporter&lt;/code&gt;) — pairs with our &lt;a href="https://bckinfo.com/mongodb-docker-compose-replica-set-authentication/" rel="noopener noreferrer"&gt;MongoDB with Docker Compose&lt;/a&gt; guide for replica set health, connection pool, and oplog metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blackbox Exporter&lt;/strong&gt; — probe external HTTP endpoints, DNS, and TCP ports from Prometheus, turning it into an uptime monitor for services outside the Docker host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana Loki&lt;/strong&gt; — add centralized log aggregation to this stack. Loki stores logs in the same way Prometheus stores metrics, and Grafana visualizes both in the same dashboard — one pane of glass for metrics and logs simultaneously.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>grafana</category>
      <category>prometheus</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>How to Install Apache Tomcat 11 on Ubuntu 26.04 LTS (Step-by-Step Guide)</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Mon, 13 Jul 2026 09:48:33 +0000</pubDate>
      <link>https://dev.to/ramansah/how-to-install-apache-tomcat-11-on-ubuntu-2604-lts-step-by-step-guide-4377</link>
      <guid>https://dev.to/ramansah/how-to-install-apache-tomcat-11-on-ubuntu-2604-lts-step-by-step-guide-4377</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is Apache Tomcat and Who Needs It&lt;/li&gt;
&lt;li&gt;Tomcat 11 vs Previous Versions: What Changed&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Step 1: Install Java 21&lt;/li&gt;
&lt;li&gt;Step 2: Create a Dedicated Tomcat User&lt;/li&gt;
&lt;li&gt;Step 3: Download and Install Tomcat 11&lt;/li&gt;
&lt;li&gt;Step 4: Configure Environment Variables&lt;/li&gt;
&lt;li&gt;Step 5: Create a systemd Service&lt;/li&gt;
&lt;li&gt;Step 6: Configure UFW Firewall&lt;/li&gt;
&lt;li&gt;Step 7: Configure Tomcat Users and Roles&lt;/li&gt;
&lt;li&gt;Step 8: Enable Remote Access to Manager GUI&lt;/li&gt;
&lt;li&gt;Step 9: Deploy a WAR Application&lt;/li&gt;
&lt;li&gt;Step 10: Put Tomcat Behind Nginx Reverse Proxy&lt;/li&gt;
&lt;li&gt;Security Hardening Checklist&lt;/li&gt;
&lt;li&gt;Tomcat vs Jetty vs Undertow: Which to Choose&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Next Steps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Java web applications don't run on their own — they need a &lt;strong&gt;servlet container&lt;/strong&gt; to manage their lifecycle, handle HTTP requests, and translate the Jakarta Servlet specification into actual running code. &lt;strong&gt;Apache Tomcat&lt;/strong&gt; has been doing exactly that since 1999, and it remains the most widely deployed open-source servlet container in the world.&lt;/p&gt;

&lt;p&gt;Whether you're deploying a Spring Boot WAR file, a legacy enterprise application, or building a Java-based REST API, this guide walks through a complete Tomcat 11 installation on Ubuntu 26.04 LTS — from Java setup through production hardening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Apache Tomcat and Who Needs It
&lt;/h2&gt;

&lt;p&gt;Tomcat is simultaneously a &lt;strong&gt;web server&lt;/strong&gt; and a &lt;strong&gt;servlet container&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As a &lt;strong&gt;web server&lt;/strong&gt;, it handles HTTP and HTTPS requests directly (though in production it typically sits behind a dedicated reverse proxy like Nginx).&lt;/li&gt;
&lt;li&gt;As a &lt;strong&gt;servlet container&lt;/strong&gt; (also called a web container), it implements the Jakarta Servlet, Jakarta Server Pages (JSP), and WebSocket specifications — the standard APIs that Java web applications are built against.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your application is packaged as a &lt;strong&gt;WAR file&lt;/strong&gt; (Web Application Archive), Tomcat is almost certainly the right deployment target. If your team is using Spring Boot and packaging as a standalone JAR with an embedded server, you may not need a standalone Tomcat installation at all — Spring Boot can embed Tomcat, Jetty, or Undertow inside the JAR itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tomcat 11 vs Previous Versions: What Changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Tomcat 9&lt;/th&gt;
&lt;th&gt;Tomcat 10&lt;/th&gt;
&lt;th&gt;Tomcat 11&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Servlet spec&lt;/td&gt;
&lt;td&gt;Servlet 4.0&lt;/td&gt;
&lt;td&gt;Servlet 5.0&lt;/td&gt;
&lt;td&gt;Servlet 6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java required&lt;/td&gt;
&lt;td&gt;Java 8+&lt;/td&gt;
&lt;td&gt;Java 11+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Java 17+&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;javax.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jakarta.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jakarta.*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (improved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual threads (Java 21)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LTS status&lt;/td&gt;
&lt;td&gt;EOL soon&lt;/td&gt;
&lt;td&gt;Maintained&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Current LTS&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The most important breaking change between Tomcat 9 and 10+:&lt;/strong&gt; the Java package namespace changed from &lt;code&gt;javax.*&lt;/code&gt; to &lt;code&gt;jakarta.*&lt;/code&gt;. Applications written for Tomcat 9 (using &lt;code&gt;javax.servlet&lt;/code&gt;) need a code change or migration tool (Eclipse Transformer) to run on Tomcat 10 or 11 — they're not drop-in compatible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual thread support in Tomcat 11 with Java 21&lt;/strong&gt; is the headline feature of this combination: Java 21's virtual threads (Project Loom) allow Tomcat to handle many more concurrent connections without the memory overhead of a large thread pool. For high-concurrency workloads, this combination delivers significantly better throughput per unit of memory compared to Tomcat 10 + Java 17.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Ubuntu 26.04 LTS (Resolute Raccoon)&lt;/li&gt;
&lt;li&gt;Minimum 2GB RAM (4GB+ recommended for production)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt; access&lt;/li&gt;
&lt;li&gt;A domain name (optional, needed for HTTPS via reverse proxy)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install Java 21
&lt;/h2&gt;

&lt;p&gt;Tomcat 11 requires Java 17 or higher. Java 21 LTS is the recommended choice — it's the current Long-Term Support release and unlocks virtual thread support:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openjdk-21-jdk-headless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openjdk version "21.x.x" ...
OpenJDK Runtime Environment (build 21.x.x+...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;JAVA_HOME&lt;/code&gt; system-wide:&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;echo&lt;/span&gt; &lt;span class="s1"&gt;'export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/profile.d/java.sh
&lt;span class="nb"&gt;source&lt;/span&gt; /etc/profile.d/java.sh

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$JAVA_HOME&lt;/span&gt;
&lt;span class="c"&gt;# /usr/lib/jvm/java-21-openjdk-amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create a Dedicated Tomcat User
&lt;/h2&gt;

&lt;p&gt;Running Tomcat as &lt;code&gt;root&lt;/code&gt; is a serious security risk — if your application has a vulnerability, an attacker would immediately have root access to the host. Create a dedicated system user with no shell access:&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;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /opt/tomcat &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/false tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mirrors the same principle of least privilege used throughout our &lt;a href="https://bckinfo.com/docker-container-security-best-practices/" rel="noopener noreferrer"&gt;Docker Container Security Best Practices&lt;/a&gt; guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Download and Install Tomcat 11
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TOMCAT_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"11.0.7"&lt;/span&gt;

wget https://dlcdn.apache.org/tomcat/tomcat-11/v&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOMCAT_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/bin/apache-tomcat-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOMCAT_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.tar.gz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/tomcat.tar.gz

&lt;span class="c"&gt;# Verify the download (recommended — check the SHA512 on the Apache download page)&lt;/span&gt;
&lt;span class="nb"&gt;sha512sum&lt;/span&gt; /tmp/tomcat.tar.gz

&lt;span class="c"&gt;# Extract to /opt/tomcat&lt;/span&gt;
&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/tomcat.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /opt/tomcat &lt;span class="nt"&gt;--strip-components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="c"&gt;# Set ownership and permissions&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; tomcat:tomcat /opt/tomcat
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;u&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rwX,g&lt;span class="o"&gt;=&lt;/span&gt;rX,o&lt;span class="o"&gt;=&lt;/span&gt; /opt/tomcat
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /opt/tomcat/bin/&lt;span class="k"&gt;*&lt;/span&gt;.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /opt/tomcat
&lt;span class="c"&gt;# bin  conf  lib  logs  temp  webapps  work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key directories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Startup and shutdown scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conf/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configuration files (&lt;code&gt;server.xml&lt;/code&gt;, &lt;code&gt;tomcat-users.xml&lt;/code&gt;, &lt;code&gt;web.xml&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webapps/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deploy your WAR files here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logs/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Catalina log, access log, error log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lib/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared libraries available to all applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 4: Configure Environment Variables
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;setenv.sh&lt;/code&gt; file in &lt;code&gt;/opt/tomcat/bin/&lt;/code&gt; — Tomcat automatically sources this at startup, the correct place for JVM tuning options:&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;nano /opt/tomcat/bin/setenv.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# JVM heap size — adjust based on available RAM&lt;/span&gt;
&lt;span class="c"&gt;# Rule of thumb: set -Xms and -Xmx to the same value to prevent heap resizing&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CATALINA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Xms512M -Xmx1024M -server"&lt;/span&gt;

&lt;span class="c"&gt;# Java 21: enable virtual threads for Loom-based connector (Tomcat 11+)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CATALINA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CATALINA_OPTS&lt;/span&gt;&lt;span class="s2"&gt; -Djava.util.concurrent.ForkJoinPool.common.parallelism=4"&lt;/span&gt;

&lt;span class="c"&gt;# Security: use /dev/urandom instead of /dev/random to avoid blocking&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"&lt;/span&gt;

&lt;span class="c"&gt;# JAVA_HOME&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/jvm/java-21-openjdk-amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the correct ownership and permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;tomcat:tomcat /opt/tomcat/bin/setenv.sh
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /opt/tomcat/bin/setenv.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Create a systemd Service
&lt;/h2&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;nano /etc/systemd/system/tomcat.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Apache Tomcat 11 Web Application Server&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://tomcat.apache.org/&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;forking&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tomcat&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tomcat&lt;/span&gt;

&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"CATALINA_HOME=/opt/tomcat"&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"CATALINA_BASE=/opt/tomcat"&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"CATALINA_PID=/opt/tomcat/temp/tomcat.pid"&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"CATALINA_OPTS=-Xms512M -Xmx1024M -server"&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"&lt;/span&gt;

&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/tomcat/bin/startup.sh&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/tomcat/bin/shutdown.sh&lt;/span&gt;

&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;SuccessExitStatus&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;143&lt;/span&gt;
&lt;span class="py"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65536&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;SyslogIdentifier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tomcat&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start:&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 daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;tomcat
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start tomcat
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&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;tomcat.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;Apache&lt;/span&gt; &lt;span class="err"&gt;Tomcat&lt;/span&gt; &lt;span class="err"&gt;11&lt;/span&gt; &lt;span class="err"&gt;Web&lt;/span&gt; &lt;span class="err"&gt;Application&lt;/span&gt; &lt;span class="err"&gt;Server&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;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; tomcat &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="c"&gt;# Or directly from Catalina log:&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /opt/tomcat/logs/catalina.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;:8080&lt;/code&gt; — you should see the Tomcat welcome page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Configure UFW Firewall
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allow Tomcat HTTP port (8080)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 8080/tcp comment &lt;span class="s2"&gt;"Tomcat HTTP"&lt;/span&gt;

&lt;span class="c"&gt;# If running HTTPS directly on Tomcat (8443)&lt;/span&gt;
&lt;span class="c"&gt;# sudo ufw allow 8443/tcp comment "Tomcat HTTPS"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Production note:&lt;/strong&gt; In a proper production setup, Tomcat should &lt;strong&gt;not&lt;/strong&gt; be directly accessible on port 8080. It should sit behind Nginx (or Nginx Proxy Manager) which handles HTTPS termination. Port 8080 should then only be accessible from &lt;code&gt;localhost&lt;/code&gt;. See Step 10 for the Nginx reverse proxy configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 7: Configure Tomcat Users and Roles
&lt;/h2&gt;

&lt;p&gt;Tomcat's web-based Manager and Host Manager applications require user credentials defined in &lt;code&gt;tomcat-users.xml&lt;/code&gt;. By default, no users are configured — the Manager UI is installed but inaccessible:&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;nano /opt/tomcat/conf/tomcat-users.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add these lines inside the &lt;code&gt;&amp;lt;tomcat-users&amp;gt;&lt;/code&gt; tag, just before &lt;code&gt;&amp;lt;/tomcat-users&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Manager GUI user — for deploying/undeploying apps via the web interface --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;role&lt;/span&gt; &lt;span class="na"&gt;rolename=&lt;/span&gt;&lt;span class="s"&gt;"manager-gui"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;role&lt;/span&gt; &lt;span class="na"&gt;rolename=&lt;/span&gt;&lt;span class="s"&gt;"manager-script"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;role&lt;/span&gt; &lt;span class="na"&gt;rolename=&lt;/span&gt;&lt;span class="s"&gt;"admin-gui"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;username=&lt;/span&gt;&lt;span class="s"&gt;"manager"&lt;/span&gt;
      &lt;span class="na"&gt;password=&lt;/span&gt;&lt;span class="s"&gt;"STRONG_MANAGER_PASSWORD_HERE"&lt;/span&gt;
      &lt;span class="na"&gt;roles=&lt;/span&gt;&lt;span class="s"&gt;"manager-gui,manager-script"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;user&lt;/span&gt; &lt;span class="na"&gt;username=&lt;/span&gt;&lt;span class="s"&gt;"admin"&lt;/span&gt;
      &lt;span class="na"&gt;password=&lt;/span&gt;&lt;span class="s"&gt;"STRONG_ADMIN_PASSWORD_HERE"&lt;/span&gt;
      &lt;span class="na"&gt;roles=&lt;/span&gt;&lt;span class="s"&gt;"manager-gui,admin-gui"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace both passwords with strong, unique values. These credentials provide full application deployment access — treat them like root passwords.&lt;/p&gt;

&lt;p&gt;Restart Tomcat to apply:&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 tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Enable Remote Access to Manager GUI
&lt;/h2&gt;

&lt;p&gt;By default, the Manager and Host Manager apps only accept connections from &lt;code&gt;localhost&lt;/code&gt; (127.0.0.1). This is a sensible security default — to access them from a remote browser, you need to explicitly allow your IP.&lt;/p&gt;

&lt;p&gt;Edit the Manager's context configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /opt/tomcat/webapps/manager/META-INF/context.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find this block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Valve&lt;/span&gt; &lt;span class="na"&gt;className=&lt;/span&gt;&lt;span class="s"&gt;"org.apache.catalina.valves.RemoteAddrValve"&lt;/span&gt;
       &lt;span class="na"&gt;allow=&lt;/span&gt;&lt;span class="s"&gt;"127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your IP address to the &lt;code&gt;allow&lt;/code&gt; pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Valve&lt;/span&gt; &lt;span class="na"&gt;className=&lt;/span&gt;&lt;span class="s"&gt;"org.apache.catalina.valves.RemoteAddrValve"&lt;/span&gt;
       &lt;span class="na"&gt;allow=&lt;/span&gt;&lt;span class="s"&gt;"127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|YOUR\.IP\.ADDRESS\.HERE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do the same for Host Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /opt/tomcat/webapps/host-manager/META-INF/context.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Tomcat:&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 tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access the Manager at &lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;:8080/manager/html&lt;/code&gt; and log in with the &lt;code&gt;manager&lt;/code&gt; credentials from &lt;code&gt;tomcat-users.xml&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Better alternative:&lt;/strong&gt; Instead of opening up the RemoteAddrValve, put Tomcat behind Nginx Proxy Manager and restrict the &lt;code&gt;/manager&lt;/code&gt; path to specific IPs via an access control list — this keeps the control in one place and doesn't require editing Tomcat config files every time your IP changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 9: Deploy a WAR Application
&lt;/h2&gt;

&lt;p&gt;Deploying a Java web application to Tomcat is straightforward — copy the WAR file to the &lt;code&gt;webapps&lt;/code&gt; directory and Tomcat auto-deploys it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy your WAR file to webapps&lt;/span&gt;
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /path/to/your-application.war /opt/tomcat/webapps/
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;tomcat:tomcat /opt/tomcat/webapps/your-application.war
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tomcat automatically extracts the WAR and deploys the application within seconds. The app becomes accessible at:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Watch the deployment in the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /opt/tomcat/logs/catalina.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deploy to ROOT context&lt;/strong&gt; (accessible at &lt;code&gt;http://your-server/&lt;/code&gt; with no path):&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;# Remove existing ROOT app first&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /opt/tomcat/webapps/ROOT
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /path/to/your-application.war /opt/tomcat/webapps/ROOT.war
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;tomcat:tomcat /opt/tomcat/webapps/ROOT.war
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Undeploy an application:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove the WAR and the extracted directory&lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /opt/tomcat/webapps/your-application.war
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /opt/tomcat/webapps/your-application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tomcat detects the removal and undeploys the application automatically without a restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Put Tomcat Behind Nginx Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;For production, Nginx handles HTTPS termination and forwards requests to Tomcat on &lt;code&gt;localhost:8080&lt;/code&gt;. This keeps port 8080 off the internet entirely:&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;nano /etc/nginx/sites-available/tomcat-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;app.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Redirect HTTP to HTTPS&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;app.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;     &lt;span class="n"&gt;/etc/letsencrypt/live/app.yourdomain.com/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/app.yourdomain.com/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Security headers&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-XSS-Protection&lt;/span&gt; &lt;span class="s"&gt;"1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;mode=block"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt;         &lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# WebSocket support&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;"upgrade"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Timeouts for long-running requests&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt;  &lt;span class="s"&gt;60s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt;    &lt;span class="s"&gt;300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_send_timeout&lt;/span&gt;    &lt;span class="s"&gt;300s&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;Enable the site and reload Nginx:&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 ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/tomcat-app /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using &lt;a href="https://bckinfo.com/nginx-proxy-manager-docker-compose-ssl-setup/" rel="noopener noreferrer"&gt;Nginx Proxy Manager&lt;/a&gt; instead of manual Nginx config, the same setup takes 30 seconds in the GUI — Forward Hostname: &lt;code&gt;localhost&lt;/code&gt;, Forward Port: &lt;code&gt;8080&lt;/code&gt;, enable Force SSL and Let's Encrypt.&lt;/p&gt;

&lt;p&gt;After setting up the reverse proxy, update Tomcat's firewall rules to only allow port 8080 from localhost:&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;# Remove public access to port 8080&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw delete allow 8080/tcp

&lt;span class="c"&gt;# Only allow from localhost (Nginx)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 127.0.0.1 to any port 8080 comment &lt;span class="s2"&gt;"Tomcat from Nginx only"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security Hardening Checklist
&lt;/h2&gt;

&lt;p&gt;A default Tomcat installation has several settings that should be changed before handling production traffic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Remove default web applications:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /opt/tomcat/webapps/examples
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /opt/tomcat/webapps/docs
&lt;span class="c"&gt;# Keep manager and host-manager only if actively used&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;examples&lt;/code&gt; app contains known vulnerabilities and has no place in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Disable the Tomcat shutdown port:&lt;/strong&gt;&lt;br&gt;
Edit &lt;code&gt;/opt/tomcat/conf/server.xml&lt;/code&gt;, change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Server&lt;/span&gt; &lt;span class="na"&gt;port=&lt;/span&gt;&lt;span class="s"&gt;"8005"&lt;/span&gt; &lt;span class="na"&gt;shutdown=&lt;/span&gt;&lt;span class="s"&gt;"SHUTDOWN"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Server&lt;/span&gt; &lt;span class="na"&gt;port=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt; &lt;span class="na"&gt;shutdown=&lt;/span&gt;&lt;span class="s"&gt;"SHUTDOWN"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This disables the shutdown command port — a common attack vector where &lt;code&gt;telnet localhost 8005&lt;/code&gt; followed by "SHUTDOWN" would stop your Tomcat instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Remove the Server header:&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;server.xml&lt;/code&gt;, inside the &lt;code&gt;&amp;lt;Connector&amp;gt;&lt;/code&gt; element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Connector&lt;/span&gt; &lt;span class="na"&gt;port=&lt;/span&gt;&lt;span class="s"&gt;"8080"&lt;/span&gt; &lt;span class="na"&gt;protocol=&lt;/span&gt;&lt;span class="s"&gt;"HTTP/1.1"&lt;/span&gt;
           &lt;span class="na"&gt;server=&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;
           &lt;span class="na"&gt;connectionTimeout=&lt;/span&gt;&lt;span class="s"&gt;"20000"&lt;/span&gt;
           &lt;span class="na"&gt;redirectPort=&lt;/span&gt;&lt;span class="s"&gt;"8443"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;server=" "&lt;/code&gt; prevents Tomcat from advertising its version in HTTP response headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Enable access logging:&lt;/strong&gt;&lt;br&gt;
Already enabled by default in &lt;code&gt;server.xml&lt;/code&gt; (&lt;code&gt;AccessLogValve&lt;/code&gt;) — confirm it's not commented out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Set secure cookie flags in &lt;code&gt;web.xml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;session-config&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;cookie-config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;http-only&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/http-only&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;secure&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/secure&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/cookie-config&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/session-config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tomcat vs Jetty vs Undertow: Which to Choose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Apache Tomcat&lt;/th&gt;
&lt;th&gt;Jetty&lt;/th&gt;
&lt;th&gt;Undertow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maturity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25+ years&lt;/td&gt;
&lt;td&gt;25+ years&lt;/td&gt;
&lt;td&gt;2013+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standalone WAR deployment&lt;/td&gt;
&lt;td&gt;Embedded, microservices&lt;/td&gt;
&lt;td&gt;Embedded, WildFly component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory footprint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Virtual threads (Java 21)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (Tomcat 11)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jakarta EE compliance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Servlet/JSP only&lt;/td&gt;
&lt;td&gt;Servlet/JSP only&lt;/td&gt;
&lt;td&gt;Via WildFly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embedded use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (Spring Boot)&lt;/td&gt;
&lt;td&gt;Yes (Spring Boot, Jetty)&lt;/td&gt;
&lt;td&gt;Yes (Spring Boot)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standalone deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community/ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Largest&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use Tomcat when:&lt;/strong&gt; you're deploying WAR files, need a proven standalone server, or are working with Spring Boot (which defaults to embedded Tomcat).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Jetty when:&lt;/strong&gt; you need an extremely lightweight embedded server, or you're building an application where startup speed matters (like AWS Lambda or serverless).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Undertow when:&lt;/strong&gt; you're already in the WildFly/JBoss ecosystem or need non-blocking I/O at the core level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat fails to start — &lt;code&gt;JAVA_HOME not set&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;JAVA_HOME&lt;/code&gt; not configured in systemd unit&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;Environment="JAVA_HOME=..."&lt;/code&gt; in &lt;code&gt;tomcat.service&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat starts but shows blank page&lt;/td&gt;
&lt;td&gt;Default ROOT app removed&lt;/td&gt;
&lt;td&gt;Copy your WAR to &lt;code&gt;webapps/ROOT.war&lt;/code&gt; or check &lt;code&gt;webapps/ROOT/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manager UI shows 403 Forbidden&lt;/td&gt;
&lt;td&gt;Remote IP not in &lt;code&gt;RemoteAddrValve&lt;/code&gt; allow list&lt;/td&gt;
&lt;td&gt;Edit &lt;code&gt;webapps/manager/META-INF/context.xml&lt;/code&gt; to add your IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404 on Manager login&lt;/td&gt;
&lt;td&gt;Wrong URL — include trailing path&lt;/td&gt;
&lt;td&gt;URL must be &lt;code&gt;http://ip:8080/manager/html&lt;/code&gt;, not &lt;code&gt;http://ip:8080/manager&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAR deployed but app returns 404&lt;/td&gt;
&lt;td&gt;App context path differs from WAR filename&lt;/td&gt;
&lt;td&gt;Access at &lt;code&gt;http://ip:8080/&amp;lt;war-filename-without-extension&amp;gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OutOfMemoryError: Java heap space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JVM heap too small&lt;/td&gt;
&lt;td&gt;Increase &lt;code&gt;-Xmx&lt;/code&gt; in &lt;code&gt;setenv.sh&lt;/code&gt; or systemd unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High CPU on first request&lt;/td&gt;
&lt;td&gt;JIT compilation warming up&lt;/td&gt;
&lt;td&gt;Normal for first few requests — use JVM warmup or AOT if critical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;With Tomcat 11 running on Ubuntu 26.04, the natural next steps for building a complete Java infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apache Maven&lt;/strong&gt; — automate the build-and-package process that produces the WAR files you deploy to Tomcat (coming next in this series).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Kafka&lt;/strong&gt; — if your Java application produces or consumes events, our &lt;a href="https://bckinfo.com/install-apache-kafka-ubuntu-26-04-lts/" rel="noopener noreferrer"&gt;Kafka installation guide&lt;/a&gt; covers the full setup on Ubuntu 26.04.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt; — the fastest way to put Tomcat behind HTTPS with a free Let's Encrypt certificate — see our &lt;a href="https://bckinfo.com/nginx-proxy-manager-docker-compose-ssl-setup/" rel="noopener noreferrer"&gt;Nginx Proxy Manager guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus + Grafana&lt;/strong&gt; — monitor Tomcat's JVM heap, thread pool, and request throughput via JMX Exporter — see the &lt;a href="https://bckinfo.com/prometheus-grafana-monitoring-stack-docker-compose/" rel="noopener noreferrer"&gt;monitoring stack guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>apache</category>
      <category>tomcat</category>
      <category>howto</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>How to Install Apache Kafka on Ubuntu 26.04 LTS (KRaft Mode, No ZooKeeper)</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Mon, 13 Jul 2026 09:41:36 +0000</pubDate>
      <link>https://dev.to/ramansah/how-to-install-apache-kafka-on-ubuntu-2604-lts-kraft-mode-no-zookeeper-1jc2</link>
      <guid>https://dev.to/ramansah/how-to-install-apache-kafka-on-ubuntu-2604-lts-kraft-mode-no-zookeeper-1jc2</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is Apache Kafka and When Should You Use It&lt;/li&gt;
&lt;li&gt;KRaft Mode: Why ZooKeeper is No Longer Needed&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Step 1: Install Java 21&lt;/li&gt;
&lt;li&gt;Step 2: Create a Dedicated Kafka User&lt;/li&gt;
&lt;li&gt;Step 3: Download and Install Apache Kafka 4.2&lt;/li&gt;
&lt;li&gt;Step 4: Configure Kafka in KRaft Mode&lt;/li&gt;
&lt;li&gt;Step 5: Format the Storage Directory&lt;/li&gt;
&lt;li&gt;Step 6: Create a systemd Service&lt;/li&gt;
&lt;li&gt;Step 7: Configure UFW Firewall&lt;/li&gt;
&lt;li&gt;Step 8: Test with Producer and Consumer&lt;/li&gt;
&lt;li&gt;Step 9: Basic Topic Management&lt;/li&gt;
&lt;li&gt;Production Configuration Tips&lt;/li&gt;
&lt;li&gt;Kafka vs RabbitMQ: When to Use Which&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Next Steps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;br&gt;
&lt;/a&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;a&gt;

&lt;p&gt;Real-time data is everywhere — microservices firing events, IoT sensors streaming telemetry, user actions triggering notifications, logs flowing from dozens of servers simultaneously. Managing all of that reliably, at scale, without building a custom integration between every producer and every consumer, is exactly the problem &lt;strong&gt;Apache Kafka&lt;/strong&gt; was built to solve.&lt;/p&gt;

&lt;p&gt;This guide walks through installing Apache Kafka 4.2 on Ubuntu 26.04 LTS (Resolute Raccoon) using &lt;strong&gt;KRaft mode&lt;/strong&gt; — the modern deployment approach that removes the ZooKeeper dependency entirely. If you've seen older Kafka tutorials that tell you to start ZooKeeper first, those are outdated: starting with Kafka 4.0, ZooKeeper support was completely removed.&lt;/p&gt;
&lt;/a&gt;&lt;h2&gt;
&lt;a&gt;
  &lt;/a&gt;
  
  What is Apache Kafka and When Should You Use It
&lt;/h2&gt;

&lt;p&gt;Apache Kafka is a &lt;strong&gt;distributed event streaming platform&lt;/strong&gt; — a durable, ordered log that producers write to and consumers read from. Unlike a traditional message queue where a message disappears after being consumed, Kafka retains events for a configurable period, allowing multiple independent consumers to read the same stream at their own pace.&lt;/p&gt;

&lt;p&gt;Real-world use cases where Kafka fits well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microservice decoupling&lt;/strong&gt; — Service A publishes an event; Services B, C, and D each consume it independently, without A knowing or caring who's listening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log and metrics aggregation&lt;/strong&gt; — Centralize logs from dozens of services into one stream, then fan out to Elasticsearch, S3, and a monitoring stack simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time analytics&lt;/strong&gt; — Process a stream of user events, transactions, or sensor readings as they happen rather than batching overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event sourcing&lt;/strong&gt; — Store every state change as an immutable event, with the ability to replay history to rebuild application state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt; — Stream database changes (inserts, updates, deletes) to downstream systems in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka is not the right tool for every messaging need. If you're sending a task to exactly one worker and want it acknowledged once, a simpler queue (Redis Streams, RabbitMQ, or even PostgreSQL LISTEN/NOTIFY) is probably sufficient and far easier to operate. Kafka earns its complexity at scale.&lt;/p&gt;
&lt;h2&gt;
  
  
  KRaft Mode: Why ZooKeeper is No Longer Needed
&lt;/h2&gt;

&lt;p&gt;Before Kafka 3.x, every Kafka deployment required a separate &lt;strong&gt;Apache ZooKeeper&lt;/strong&gt; cluster to manage broker metadata, leader elections, and cluster state. This meant running and maintaining two distributed systems for every Kafka deployment — doubling the operational complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KRaft (Kafka Raft Metadata mode)&lt;/strong&gt; replaces ZooKeeper by moving cluster metadata management directly into Kafka itself, using the Raft consensus algorithm. The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simpler deployment&lt;/strong&gt; — one system to install, configure, monitor, and upgrade instead of two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster startup and failover&lt;/strong&gt; — Kafka no longer needs to synchronize with an external ZooKeeper cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better scalability&lt;/strong&gt; — ZooKeeper had practical limits on the number of partitions it could track; KRaft removes those limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka 4.0+: ZooKeeper support is completely removed.&lt;/strong&gt; There is no option to use ZooKeeper with Kafka 4.x — KRaft is the only deployment mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu 26.04 LTS (Resolute Raccoon) — fresh install or existing server&lt;/li&gt;
&lt;li&gt;Minimum 4GB RAM (8GB+ recommended for production workloads)&lt;/li&gt;
&lt;li&gt;At least 2 CPU cores&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt; access&lt;/li&gt;
&lt;li&gt;Internet access to download packages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 1: Install Java 21
&lt;/h2&gt;

&lt;p&gt;Kafka 4.x requires Java 17 or higher. Java 21 LTS is the recommended choice for Ubuntu 26.04 — it's the current Long-Term Support release and ships cleanly from Ubuntu's default 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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openjdk-21-jdk-headless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openjdk version "21.x.x" ...
OpenJDK Runtime Environment (build 21.x.x+...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;JAVA_HOME&lt;/code&gt; so Kafka can find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/profile.d/java.sh
&lt;span class="nb"&gt;source&lt;/span&gt; /etc/profile.d/java.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm:&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;echo&lt;/span&gt; &lt;span class="nv"&gt;$JAVA_HOME&lt;/span&gt;
&lt;span class="c"&gt;# /usr/lib/jvm/java-21-openjdk-amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create a Dedicated Kafka User
&lt;/h2&gt;

&lt;p&gt;Running Kafka as &lt;code&gt;root&lt;/code&gt; is a security risk. Create a dedicated system user with no login access:&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;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /opt/kafka &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/false kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the same principle of least privilege covered in our &lt;a href="https://bckinfo.com/docker-container-security-best-practices/" rel="noopener noreferrer"&gt;Docker Container Security Best Practices&lt;/a&gt; guide — services should run with the minimum access they need, never as root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Download and Install Apache Kafka 4.2
&lt;/h2&gt;

&lt;p&gt;Set the version variables, then download and extract to &lt;code&gt;/opt/kafka&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="nv"&gt;KAFKA_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4.2.0"&lt;/span&gt;
&lt;span class="nv"&gt;SCALA_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2.13"&lt;/span&gt;

wget https://downloads.apache.org/kafka/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KAFKA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/kafka_&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCALA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KAFKA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.tgz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/kafka.tgz

&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/kafka.tgz &lt;span class="nt"&gt;-C&lt;/span&gt; /opt/kafka &lt;span class="nt"&gt;--strip-components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; kafka:kafka /opt/kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the extraction:&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;ls&lt;/span&gt; /opt/kafka
&lt;span class="c"&gt;# bin  config  libs  LICENSE  licenses  NOTICE  site-docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a dedicated data directory for Kafka logs (separate from the application directory — good practice for disk management and backups):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Configure Kafka in KRaft Mode
&lt;/h2&gt;

&lt;p&gt;KRaft mode uses a single unified configuration file (&lt;code&gt;server.properties&lt;/code&gt;) where the broker also acts as the cluster controller. Edit the KRaft server configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /opt/kafka/config/kraft/server.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key settings to review and update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# The role this node plays in the cluster
# "broker,controller" = combined mode (suitable for single-node and small clusters)
&lt;/span&gt;&lt;span class="py"&gt;process.roles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;broker,controller&lt;/span&gt;

&lt;span class="c"&gt;# Unique ID for this broker — change if running multiple brokers
&lt;/span&gt;&lt;span class="py"&gt;node.id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;

&lt;span class="c"&gt;# Controller quorum voters — format: node.id@host:port
# For single node: use the same node
&lt;/span&gt;&lt;span class="py"&gt;controller.quorum.voters&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1@localhost:9093&lt;/span&gt;

&lt;span class="c"&gt;# Listeners — what addresses Kafka binds to
&lt;/span&gt;&lt;span class="py"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT://localhost:9092,CONTROLLER://localhost:9093&lt;/span&gt;

&lt;span class="c"&gt;# The address clients use to reach the broker
# Change 'localhost' to your server's actual IP or hostname if remote clients need access
&lt;/span&gt;&lt;span class="py"&gt;advertised.listeners&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT://localhost:9092&lt;/span&gt;

&lt;span class="c"&gt;# Listener used for inter-broker and controller communication
&lt;/span&gt;&lt;span class="py"&gt;inter.broker.listener.name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT&lt;/span&gt;
&lt;span class="py"&gt;controller.listener.names&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CONTROLLER&lt;/span&gt;

&lt;span class="c"&gt;# Log directory — where Kafka stores event data
&lt;/span&gt;&lt;span class="py"&gt;log.dirs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/kafka/data&lt;/span&gt;

&lt;span class="c"&gt;# Number of partitions for auto-created topics
&lt;/span&gt;&lt;span class="py"&gt;num.partitions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;

&lt;span class="c"&gt;# Log retention (how long to keep events)
&lt;/span&gt;&lt;span class="py"&gt;log.retention.hours&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;168        # 7 days&lt;/span&gt;
&lt;span class="py"&gt;log.retention.bytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1073741824 # 1GB per partition&lt;/span&gt;

&lt;span class="c"&gt;# Replication factor for internal topics (keep at 1 for single-node)
&lt;/span&gt;&lt;span class="py"&gt;offsets.topic.replication.factor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;transaction.state.log.replication.factor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;transaction.state.log.min.isr&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Remote access note:&lt;/strong&gt; If clients will connect from other machines, replace &lt;code&gt;localhost&lt;/code&gt; in &lt;code&gt;advertised.listeners&lt;/code&gt; with your server's actual IP address or DNS hostname. Kafka clients use the advertised address to establish connections — using &lt;code&gt;localhost&lt;/code&gt; here will cause remote clients to fail even if they can reach the server on port 9092.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Format the Storage Directory
&lt;/h2&gt;

&lt;p&gt;Before first startup, Kafka's storage directory must be initialized with a unique cluster ID. This is a one-time operation:&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;# Generate a cluster ID&lt;/span&gt;
&lt;span class="nv"&gt;CLUSTER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/opt/kafka/bin/kafka-storage.sh random-uuid&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Cluster ID: &lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Format the storage directory with that cluster ID&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-storage.sh format &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$CLUSTER_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; /opt/kafka/config/kraft/server.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Formatting /var/lib/kafka/data with metadata.version X.X-IV...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step replaces the old ZooKeeper initialization — there's no &lt;code&gt;zookeeper-server-start.sh&lt;/code&gt; to run first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Create a systemd Service
&lt;/h2&gt;

&lt;p&gt;Create a systemd unit file so Kafka starts automatically on boot and can be managed with &lt;code&gt;systemctl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/systemd/system/kafka.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Apache Kafka Server (KRaft Mode)&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://kafka.apache.org/documentation/&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/kafka/bin/kafka-server-start.sh &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;/opt/kafka/config/kraft/server.properties&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/kafka/bin/kafka-server-stop.sh&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65536&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;SyslogIdentifier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;kafka
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the status:&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 status kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;●&lt;/span&gt; &lt;span class="err"&gt;kafka.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;Apache&lt;/span&gt; &lt;span class="err"&gt;Kafka&lt;/span&gt; &lt;span class="err"&gt;Server&lt;/span&gt; &lt;span class="err"&gt;(KRaft&lt;/span&gt; &lt;span class="err"&gt;Mode)&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/kafka.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;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; kafka &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Configure UFW Firewall
&lt;/h2&gt;

&lt;p&gt;If UFW is enabled, open the ports Kafka needs:&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;# Port 9092 — Kafka broker (clients connect here)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 9092/tcp comment &lt;span class="s2"&gt;"Kafka broker"&lt;/span&gt;

&lt;span class="c"&gt;# Port 9093 — Kafka controller (internal KRaft communication)&lt;/span&gt;
&lt;span class="c"&gt;# Only needed if running multi-node — restrict to internal network for single-node&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 10.0.0.0/8 to any port 9093 comment &lt;span class="s2"&gt;"Kafka KRaft controller"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Port 9092 should only be open to trusted clients, not the entire internet. If your Kafka broker is internet-facing, restrict port 9092 to specific IP ranges or put it behind a VPN.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8: Test with Producer and Consumer
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;two separate terminal sessions&lt;/strong&gt; for this test — one for the producer, one for the consumer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: Create a test topic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partitions&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replication-factor&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Created topic test-events.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terminal 1: Start a producer and send messages&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-console-producer.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type a few messages and press Enter after each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Hello from Kafka on Ubuntu 26.04
&amp;gt; This is a test message
&amp;gt; Apache Kafka 4.2 KRaft mode works!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; to exit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal 2: Start a consumer and read messages&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-console-consumer.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-beginning&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the messages you typed appear immediately. Press &lt;code&gt;Ctrl+C&lt;/code&gt; to exit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--from-beginning&lt;/code&gt; tells the consumer to read from the earliest available offset — not just new messages. This illustrates one of Kafka's key properties: consumers can replay historical events, not just receive new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Basic Topic Management
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all topics&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Describe a topic (partitions, replicas, leader)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--describe&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Increase partition count (partitions can only be increased, never decreased)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alter&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partitions&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Delete a topic&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# List consumer groups&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-consumer-groups.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Check consumer group lag (how far behind a consumer is)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-consumer-groups.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--describe&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group&lt;/span&gt; my-consumer-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumer group lag (last command) is one of the most important operational metrics in a Kafka deployment — it tells you how many messages a consumer is behind. Integrating this metric into your &lt;a href="https://bckinfo.com/prometheus-grafana-monitoring-stack-docker-compose/" rel="noopener noreferrer"&gt;Prometheus + Grafana monitoring stack&lt;/a&gt; via &lt;code&gt;kafka-exporter&lt;/code&gt; or JMX exporter gives you real-time visibility into consumer health.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Configuration Tips
&lt;/h2&gt;

&lt;p&gt;A single-node Kafka installation on Ubuntu is sufficient for development and low-traffic production workloads. For anything that needs to scale or survive a broker failure, here are the key settings to revisit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory tuning:&lt;/strong&gt;&lt;br&gt;
Kafka's JVM heap is set to 1GB by default. For production, adjust in &lt;code&gt;/opt/kafka/bin/kafka-server-start.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KAFKA_HEAP_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx4G -Xms4G"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set both &lt;code&gt;-Xmx&lt;/code&gt; and &lt;code&gt;-Xms&lt;/code&gt; to the same value to prevent JVM heap resizing pauses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log retention by size, not just time:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Retain logs for 7 days OR until they exceed 10GB per partition, whichever comes first
&lt;/span&gt;&lt;span class="py"&gt;log.retention.hours&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;168&lt;/span&gt;
&lt;span class="py"&gt;log.retention.bytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10737418240&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auto topic creation in production:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disable auto-creation — require topics to be created explicitly
&lt;/span&gt;&lt;span class="py"&gt;auto.create.topics.enable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-created topics use default partition/replication settings, which are rarely correct for specific use cases. Disable this and create topics explicitly with the right parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication factor for multi-broker clusters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# With 3 brokers, use replication factor 3 and min ISR 2
# (data survives loss of any 1 broker, writes require 2 brokers to acknowledge)
&lt;/span&gt;&lt;span class="py"&gt;default.replication.factor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;min.insync.replicas&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Regular backups:&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;/var/lib/kafka/data&lt;/code&gt; directory contains all event data. Back it up the same way you'd back up any other data volume — a scheduled snapshot with retention policy, tested restore process, stored separately from the host. The scripted approach from our &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;Redis with Docker Compose&lt;/a&gt; guide applies directly to the Kafka data directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kafka vs RabbitMQ: When to Use Which
&lt;/h2&gt;

&lt;p&gt;A common question when introducing a message broker for the first time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Apache Kafka&lt;/th&gt;
&lt;th&gt;RabbitMQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distributed log (pull-based)&lt;/td&gt;
&lt;td&gt;Message queue (push-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Message retention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configurable period (days/weeks)&lt;/td&gt;
&lt;td&gt;Until acknowledged (usually)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Millions of messages/second&lt;/td&gt;
&lt;td&gt;Tens to hundreds of thousands/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumer pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each consumer group reads the full log independently&lt;/td&gt;
&lt;td&gt;One consumer per message (competing consumers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — consumers can re-read past events&lt;/td&gt;
&lt;td&gt;No — once consumed, message is gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ordering guarantee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per partition&lt;/td&gt;
&lt;td&gt;Per queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event streaming, log aggregation, CDC, high throughput&lt;/td&gt;
&lt;td&gt;Task queues, RPC, point-to-point messaging, simpler use cases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Choose Kafka when:&lt;/strong&gt; You need multiple consumers to independently process the same events, you need to replay history, or you need to handle very high throughput (millions of events/second).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose RabbitMQ when:&lt;/strong&gt; You're building a task queue where each task should be processed exactly once by exactly one worker, the message volume is moderate, and you want simpler operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;kafka.service&lt;/code&gt; fails to start&lt;/td&gt;
&lt;td&gt;Storage not formatted, or wrong &lt;code&gt;JAVA_HOME&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;kafka-storage.sh format&lt;/code&gt; first; verify &lt;code&gt;java -version&lt;/code&gt; works as kafka user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Connection refused&lt;/code&gt; on port 9092&lt;/td&gt;
&lt;td&gt;Kafka not running, or listener bound to wrong address&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;systemctl status kafka&lt;/code&gt;; verify &lt;code&gt;listeners&lt;/code&gt; in &lt;code&gt;server.properties&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote clients can't connect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;advertised.listeners&lt;/code&gt; set to &lt;code&gt;localhost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Change &lt;code&gt;advertised.listeners&lt;/code&gt; to the server's actual IP or hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Leader not available&lt;/code&gt; on topic create&lt;/td&gt;
&lt;td&gt;Kafka still initializing after start&lt;/td&gt;
&lt;td&gt;Wait 10-15 seconds after Kafka starts before creating topics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer reads no messages&lt;/td&gt;
&lt;td&gt;Consumer started after producer, no &lt;code&gt;--from-beginning&lt;/code&gt; flag&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;--from-beginning&lt;/code&gt; flag to read historical messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;OutOfMemoryError&lt;/code&gt; in logs&lt;/td&gt;
&lt;td&gt;JVM heap too small for the workload&lt;/td&gt;
&lt;td&gt;Increase &lt;code&gt;-Xmx&lt;/code&gt; and &lt;code&gt;-Xms&lt;/code&gt; in &lt;code&gt;kafka-server-start.sh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High consumer group lag&lt;/td&gt;
&lt;td&gt;Consumer too slow or too few consumer instances&lt;/td&gt;
&lt;td&gt;Scale consumer instances; check processing logic for bottlenecks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;With Kafka running on Ubuntu 26.04, the natural next steps are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add monitoring&lt;/strong&gt; — deploy &lt;code&gt;kafka-exporter&lt;/code&gt; alongside your &lt;a href="https://bckinfo.com/prometheus-grafana-monitoring-stack-docker-compose/" rel="noopener noreferrer"&gt;Prometheus + Grafana stack&lt;/a&gt; to track consumer group lag, message throughput, and broker health in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containerize it&lt;/strong&gt; — for teams already running &lt;a href="https://bckinfo.com/install-docker-docker-compose-ubuntu-26-04-lts/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; stacks, Kafka has an official Docker image and can be added to an existing Compose setup using the same KRaft configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore Kafka Streams or Apache Flink&lt;/strong&gt; — for in-stream processing (filtering, aggregating, joining streams) without writing a separate consumer application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale to multi-broker&lt;/strong&gt; — repeat the installation on additional nodes, adjust &lt;code&gt;broker.id&lt;/code&gt; and &lt;code&gt;controller.quorum.voters&lt;/code&gt;, and distribute your topics' partitions across the cluster for fault tolerance and horizontal throughput scaling.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kafka</category>
      <category>howto</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>How to Install Apache Kafka on Ubuntu 26.04 LTS (KRaft Mode, No ZooKeeper)</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Sun, 12 Jul 2026 16:01:06 +0000</pubDate>
      <link>https://dev.to/ramansah/how-to-install-apache-kafka-on-ubuntu-2604-lts-kraft-mode-no-zookeeper-408a</link>
      <guid>https://dev.to/ramansah/how-to-install-apache-kafka-on-ubuntu-2604-lts-kraft-mode-no-zookeeper-408a</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is Apache Kafka and When Should You Use It&lt;/li&gt;
&lt;li&gt;KRaft Mode: Why ZooKeeper is No Longer Needed&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Step 1: Install Java 21&lt;/li&gt;
&lt;li&gt;Step 2: Create a Dedicated Kafka User&lt;/li&gt;
&lt;li&gt;Step 3: Download and Install Apache Kafka 4.2&lt;/li&gt;
&lt;li&gt;Step 4: Configure Kafka in KRaft Mode&lt;/li&gt;
&lt;li&gt;Step 5: Format the Storage Directory&lt;/li&gt;
&lt;li&gt;Step 6: Create a systemd Service&lt;/li&gt;
&lt;li&gt;Step 7: Configure UFW Firewall&lt;/li&gt;
&lt;li&gt;Step 8: Test with Producer and Consumer&lt;/li&gt;
&lt;li&gt;Step 9: Basic Topic Management&lt;/li&gt;
&lt;li&gt;Production Configuration Tips&lt;/li&gt;
&lt;li&gt;Kafka vs RabbitMQ: When to Use Which&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Next Steps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real-time data is everywhere — microservices firing events, IoT sensors streaming telemetry, user actions triggering notifications, logs flowing from dozens of servers simultaneously. Managing all of that reliably, at scale, without building a custom integration between every producer and every consumer, is exactly the problem &lt;strong&gt;Apache Kafka&lt;/strong&gt; was built to solve.&lt;/p&gt;

&lt;p&gt;This guide walks through installing Apache Kafka 4.2 on Ubuntu 26.04 LTS (Resolute Raccoon) using &lt;strong&gt;KRaft mode&lt;/strong&gt; — the modern deployment approach that removes the ZooKeeper dependency entirely. If you've seen older Kafka tutorials that tell you to start ZooKeeper first, those are outdated: starting with Kafka 4.0, ZooKeeper support was completely removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Apache Kafka and When Should You Use It
&lt;/h2&gt;

&lt;p&gt;Apache Kafka is a &lt;strong&gt;distributed event streaming platform&lt;/strong&gt; — a durable, ordered log that producers write to and consumers read from. Unlike a traditional message queue where a message disappears after being consumed, Kafka retains events for a configurable period, allowing multiple independent consumers to read the same stream at their own pace.&lt;/p&gt;

&lt;p&gt;Real-world use cases where Kafka fits well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microservice decoupling&lt;/strong&gt; — Service A publishes an event; Services B, C, and D each consume it independently, without A knowing or caring who's listening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log and metrics aggregation&lt;/strong&gt; — Centralize logs from dozens of services into one stream, then fan out to Elasticsearch, S3, and a monitoring stack simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time analytics&lt;/strong&gt; — Process a stream of user events, transactions, or sensor readings as they happen rather than batching overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event sourcing&lt;/strong&gt; — Store every state change as an immutable event, with the ability to replay history to rebuild application state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt; — Stream database changes (inserts, updates, deletes) to downstream systems in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka is not the right tool for every messaging need. If you're sending a task to exactly one worker and want it acknowledged once, a simpler queue (Redis Streams, RabbitMQ, or even PostgreSQL LISTEN/NOTIFY) is probably sufficient and far easier to operate. Kafka earns its complexity at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  KRaft Mode: Why ZooKeeper is No Longer Needed
&lt;/h2&gt;

&lt;p&gt;Before Kafka 3.x, every Kafka deployment required a separate &lt;strong&gt;Apache ZooKeeper&lt;/strong&gt; cluster to manage broker metadata, leader elections, and cluster state. This meant running and maintaining two distributed systems for every Kafka deployment — doubling the operational complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KRaft (Kafka Raft Metadata mode)&lt;/strong&gt; replaces ZooKeeper by moving cluster metadata management directly into Kafka itself, using the Raft consensus algorithm. The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simpler deployment&lt;/strong&gt; — one system to install, configure, monitor, and upgrade instead of two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster startup and failover&lt;/strong&gt; — Kafka no longer needs to synchronize with an external ZooKeeper cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better scalability&lt;/strong&gt; — ZooKeeper had practical limits on the number of partitions it could track; KRaft removes those limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka 4.0+: ZooKeeper support is completely removed.&lt;/strong&gt; There is no option to use ZooKeeper with Kafka 4.x — KRaft is the only deployment mode.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Ubuntu 26.04 LTS (Resolute Raccoon) — fresh install or existing server&lt;/li&gt;
&lt;li&gt;Minimum 4GB RAM (8GB+ recommended for production workloads)&lt;/li&gt;
&lt;li&gt;At least 2 CPU cores&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt; access&lt;/li&gt;
&lt;li&gt;Internet access to download packages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install Java 21
&lt;/h2&gt;

&lt;p&gt;Kafka 4.x requires Java 17 or higher. Java 21 LTS is the recommended choice for Ubuntu 26.04 — it's the current Long-Term Support release and ships cleanly from Ubuntu's default 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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openjdk-21-jdk-headless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openjdk version "21.x.x" ...
OpenJDK Runtime Environment (build 21.x.x+...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;JAVA_HOME&lt;/code&gt; so Kafka can find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/profile.d/java.sh
&lt;span class="nb"&gt;source&lt;/span&gt; /etc/profile.d/java.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm:&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;echo&lt;/span&gt; &lt;span class="nv"&gt;$JAVA_HOME&lt;/span&gt;
&lt;span class="c"&gt;# /usr/lib/jvm/java-21-openjdk-amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create a Dedicated Kafka User
&lt;/h2&gt;

&lt;p&gt;Running Kafka as &lt;code&gt;root&lt;/code&gt; is a security risk. Create a dedicated system user with no login access:&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;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /opt/kafka &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/false kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the same principle of least privilege covered in our &lt;a href="https://bckinfo.com/docker-container-security-best-practices/" rel="noopener noreferrer"&gt;Docker Container Security Best Practices&lt;/a&gt; guide — services should run with the minimum access they need, never as root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Download and Install Apache Kafka 4.2
&lt;/h2&gt;

&lt;p&gt;Set the version variables, then download and extract to &lt;code&gt;/opt/kafka&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="nv"&gt;KAFKA_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4.2.0"&lt;/span&gt;
&lt;span class="nv"&gt;SCALA_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2.13"&lt;/span&gt;

wget https://downloads.apache.org/kafka/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KAFKA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/kafka_&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SCALA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KAFKA_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.tgz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/kafka.tgz

&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/kafka.tgz &lt;span class="nt"&gt;-C&lt;/span&gt; /opt/kafka &lt;span class="nt"&gt;--strip-components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; kafka:kafka /opt/kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the extraction:&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;ls&lt;/span&gt; /opt/kafka
&lt;span class="c"&gt;# bin  config  libs  LICENSE  licenses  NOTICE  site-docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a dedicated data directory for Kafka logs (separate from the application directory — good practice for disk management and backups):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Configure Kafka in KRaft Mode
&lt;/h2&gt;

&lt;p&gt;KRaft mode uses a single unified configuration file (&lt;code&gt;server.properties&lt;/code&gt;) where the broker also acts as the cluster controller. Edit the KRaft server configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /opt/kafka/config/kraft/server.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key settings to review and update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# The role this node plays in the cluster
# "broker,controller" = combined mode (suitable for single-node and small clusters)
&lt;/span&gt;&lt;span class="py"&gt;process.roles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;broker,controller&lt;/span&gt;

&lt;span class="c"&gt;# Unique ID for this broker — change if running multiple brokers
&lt;/span&gt;&lt;span class="py"&gt;node.id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;

&lt;span class="c"&gt;# Controller quorum voters — format: node.id@host:port
# For single node: use the same node
&lt;/span&gt;&lt;span class="py"&gt;controller.quorum.voters&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1@localhost:9093&lt;/span&gt;

&lt;span class="c"&gt;# Listeners — what addresses Kafka binds to
&lt;/span&gt;&lt;span class="py"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT://localhost:9092,CONTROLLER://localhost:9093&lt;/span&gt;

&lt;span class="c"&gt;# The address clients use to reach the broker
# Change 'localhost' to your server's actual IP or hostname if remote clients need access
&lt;/span&gt;&lt;span class="py"&gt;advertised.listeners&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT://localhost:9092&lt;/span&gt;

&lt;span class="c"&gt;# Listener used for inter-broker and controller communication
&lt;/span&gt;&lt;span class="py"&gt;inter.broker.listener.name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PLAINTEXT&lt;/span&gt;
&lt;span class="py"&gt;controller.listener.names&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CONTROLLER&lt;/span&gt;

&lt;span class="c"&gt;# Log directory — where Kafka stores event data
&lt;/span&gt;&lt;span class="py"&gt;log.dirs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/lib/kafka/data&lt;/span&gt;

&lt;span class="c"&gt;# Number of partitions for auto-created topics
&lt;/span&gt;&lt;span class="py"&gt;num.partitions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;

&lt;span class="c"&gt;# Log retention (how long to keep events)
&lt;/span&gt;&lt;span class="py"&gt;log.retention.hours&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;168        # 7 days&lt;/span&gt;
&lt;span class="py"&gt;log.retention.bytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1073741824 # 1GB per partition&lt;/span&gt;

&lt;span class="c"&gt;# Replication factor for internal topics (keep at 1 for single-node)
&lt;/span&gt;&lt;span class="py"&gt;offsets.topic.replication.factor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;transaction.state.log.replication.factor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;transaction.state.log.min.isr&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Remote access note:&lt;/strong&gt; If clients will connect from other machines, replace &lt;code&gt;localhost&lt;/code&gt; in &lt;code&gt;advertised.listeners&lt;/code&gt; with your server's actual IP address or DNS hostname. Kafka clients use the advertised address to establish connections — using &lt;code&gt;localhost&lt;/code&gt; here will cause remote clients to fail even if they can reach the server on port 9092.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Format the Storage Directory
&lt;/h2&gt;

&lt;p&gt;Before first startup, Kafka's storage directory must be initialized with a unique cluster ID. This is a one-time operation:&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;# Generate a cluster ID&lt;/span&gt;
&lt;span class="nv"&gt;CLUSTER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/opt/kafka/bin/kafka-storage.sh random-uuid&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Cluster ID: &lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Format the storage directory with that cluster ID&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-storage.sh format &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nv"&gt;$CLUSTER_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; /opt/kafka/config/kraft/server.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Formatting /var/lib/kafka/data with metadata.version X.X-IV...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step replaces the old ZooKeeper initialization — there's no &lt;code&gt;zookeeper-server-start.sh&lt;/code&gt; to run first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Create a systemd Service
&lt;/h2&gt;

&lt;p&gt;Create a systemd unit file so Kafka starts automatically on boot and can be managed with &lt;code&gt;systemctl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/systemd/system/kafka.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Apache Kafka Server (KRaft Mode)&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://kafka.apache.org/documentation/&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/kafka/bin/kafka-server-start.sh &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;/opt/kafka/config/kraft/server.properties&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/kafka/bin/kafka-server-stop.sh&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65536&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;SyslogIdentifier&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;kafka&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;kafka
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the status:&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 status kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;●&lt;/span&gt; &lt;span class="err"&gt;kafka.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;Apache&lt;/span&gt; &lt;span class="err"&gt;Kafka&lt;/span&gt; &lt;span class="err"&gt;Server&lt;/span&gt; &lt;span class="err"&gt;(KRaft&lt;/span&gt; &lt;span class="err"&gt;Mode)&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/kafka.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;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; kafka &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Configure UFW Firewall
&lt;/h2&gt;

&lt;p&gt;If UFW is enabled, open the ports Kafka needs:&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;# Port 9092 — Kafka broker (clients connect here)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 9092/tcp comment &lt;span class="s2"&gt;"Kafka broker"&lt;/span&gt;

&lt;span class="c"&gt;# Port 9093 — Kafka controller (internal KRaft communication)&lt;/span&gt;
&lt;span class="c"&gt;# Only needed if running multi-node — restrict to internal network for single-node&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 10.0.0.0/8 to any port 9093 comment &lt;span class="s2"&gt;"Kafka KRaft controller"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Port 9092 should only be open to trusted clients, not the entire internet. If your Kafka broker is internet-facing, restrict port 9092 to specific IP ranges or put it behind a VPN.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8: Test with Producer and Consumer
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;two separate terminal sessions&lt;/strong&gt; for this test — one for the producer, one for the consumer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: Create a test topic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partitions&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replication-factor&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Created topic test-events.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terminal 1: Start a producer and send messages&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-console-producer.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type a few messages and press Enter after each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Hello from Kafka on Ubuntu 26.04
&amp;gt; This is a test message
&amp;gt; Apache Kafka 4.2 KRaft mode works!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; to exit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal 2: Start a consumer and read messages&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-console-consumer.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-beginning&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the messages you typed appear immediately. Press &lt;code&gt;Ctrl+C&lt;/code&gt; to exit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--from-beginning&lt;/code&gt; tells the consumer to read from the earliest available offset — not just new messages. This illustrates one of Kafka's key properties: consumers can replay historical events, not just receive new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Basic Topic Management
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all topics&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Describe a topic (partitions, replicas, leader)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--describe&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Increase partition count (partitions can only be increased, never decreased)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alter&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partitions&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Delete a topic&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-topics.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; test-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# List consumer groups&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-consumer-groups.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092

&lt;span class="c"&gt;# Check consumer group lag (how far behind a consumer is)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; kafka /opt/kafka/bin/kafka-consumer-groups.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--describe&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group&lt;/span&gt; my-consumer-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumer group lag (last command) is one of the most important operational metrics in a Kafka deployment — it tells you how many messages a consumer is behind. Integrating this metric into your &lt;a href="https://bckinfo.com/prometheus-grafana-monitoring-stack-docker-compose/" rel="noopener noreferrer"&gt;Prometheus + Grafana monitoring stack&lt;/a&gt; via &lt;code&gt;kafka-exporter&lt;/code&gt; or JMX exporter gives you real-time visibility into consumer health.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Configuration Tips
&lt;/h2&gt;

&lt;p&gt;A single-node Kafka installation on Ubuntu is sufficient for development and low-traffic production workloads. For anything that needs to scale or survive a broker failure, here are the key settings to revisit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory tuning:&lt;/strong&gt;&lt;br&gt;
Kafka's JVM heap is set to 1GB by default. For production, adjust in &lt;code&gt;/opt/kafka/bin/kafka-server-start.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KAFKA_HEAP_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx4G -Xms4G"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set both &lt;code&gt;-Xmx&lt;/code&gt; and &lt;code&gt;-Xms&lt;/code&gt; to the same value to prevent JVM heap resizing pauses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log retention by size, not just time:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Retain logs for 7 days OR until they exceed 10GB per partition, whichever comes first
&lt;/span&gt;&lt;span class="py"&gt;log.retention.hours&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;168&lt;/span&gt;
&lt;span class="py"&gt;log.retention.bytes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10737418240&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auto topic creation in production:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disable auto-creation — require topics to be created explicitly
&lt;/span&gt;&lt;span class="py"&gt;auto.create.topics.enable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-created topics use default partition/replication settings, which are rarely correct for specific use cases. Disable this and create topics explicitly with the right parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication factor for multi-broker clusters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# With 3 brokers, use replication factor 3 and min ISR 2
# (data survives loss of any 1 broker, writes require 2 brokers to acknowledge)
&lt;/span&gt;&lt;span class="py"&gt;default.replication.factor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;min.insync.replicas&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Regular backups:&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;/var/lib/kafka/data&lt;/code&gt; directory contains all event data. Back it up the same way you'd back up any other data volume — a scheduled snapshot with retention policy, tested restore process, stored separately from the host. The scripted approach from our &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;Redis with Docker Compose&lt;/a&gt; guide applies directly to the Kafka data directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kafka vs RabbitMQ: When to Use Which
&lt;/h2&gt;

&lt;p&gt;A common question when introducing a message broker for the first time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Apache Kafka&lt;/th&gt;
&lt;th&gt;RabbitMQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distributed log (pull-based)&lt;/td&gt;
&lt;td&gt;Message queue (push-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Message retention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configurable period (days/weeks)&lt;/td&gt;
&lt;td&gt;Until acknowledged (usually)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Millions of messages/second&lt;/td&gt;
&lt;td&gt;Tens to hundreds of thousands/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumer pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each consumer group reads the full log independently&lt;/td&gt;
&lt;td&gt;One consumer per message (competing consumers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — consumers can re-read past events&lt;/td&gt;
&lt;td&gt;No — once consumed, message is gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ordering guarantee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per partition&lt;/td&gt;
&lt;td&gt;Per queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event streaming, log aggregation, CDC, high throughput&lt;/td&gt;
&lt;td&gt;Task queues, RPC, point-to-point messaging, simpler use cases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Choose Kafka when:&lt;/strong&gt; You need multiple consumers to independently process the same events, you need to replay history, or you need to handle very high throughput (millions of events/second).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose RabbitMQ when:&lt;/strong&gt; You're building a task queue where each task should be processed exactly once by exactly one worker, the message volume is moderate, and you want simpler operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;kafka.service&lt;/code&gt; fails to start&lt;/td&gt;
&lt;td&gt;Storage not formatted, or wrong &lt;code&gt;JAVA_HOME&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;kafka-storage.sh format&lt;/code&gt; first; verify &lt;code&gt;java -version&lt;/code&gt; works as kafka user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Connection refused&lt;/code&gt; on port 9092&lt;/td&gt;
&lt;td&gt;Kafka not running, or listener bound to wrong address&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;systemctl status kafka&lt;/code&gt;; verify &lt;code&gt;listeners&lt;/code&gt; in &lt;code&gt;server.properties&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote clients can't connect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;advertised.listeners&lt;/code&gt; set to &lt;code&gt;localhost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Change &lt;code&gt;advertised.listeners&lt;/code&gt; to the server's actual IP or hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Leader not available&lt;/code&gt; on topic create&lt;/td&gt;
&lt;td&gt;Kafka still initializing after start&lt;/td&gt;
&lt;td&gt;Wait 10-15 seconds after Kafka starts before creating topics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer reads no messages&lt;/td&gt;
&lt;td&gt;Consumer started after producer, no &lt;code&gt;--from-beginning&lt;/code&gt; flag&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;--from-beginning&lt;/code&gt; flag to read historical messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;OutOfMemoryError&lt;/code&gt; in logs&lt;/td&gt;
&lt;td&gt;JVM heap too small for the workload&lt;/td&gt;
&lt;td&gt;Increase &lt;code&gt;-Xmx&lt;/code&gt; and &lt;code&gt;-Xms&lt;/code&gt; in &lt;code&gt;kafka-server-start.sh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High consumer group lag&lt;/td&gt;
&lt;td&gt;Consumer too slow or too few consumer instances&lt;/td&gt;
&lt;td&gt;Scale consumer instances; check processing logic for bottlenecks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;With Kafka running on Ubuntu 26.04, the natural next steps are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add monitoring&lt;/strong&gt; — deploy &lt;code&gt;kafka-exporter&lt;/code&gt; alongside your &lt;a href="https://bckinfo.com/prometheus-grafana-monitoring-stack-docker-compose/" rel="noopener noreferrer"&gt;Prometheus + Grafana stack&lt;/a&gt; to track consumer group lag, message throughput, and broker health in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containerize it&lt;/strong&gt; — for teams already running &lt;a href="https://bckinfo.com/install-docker-docker-compose-ubuntu-26-04-lts/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; stacks, Kafka has an official Docker image and can be added to an existing Compose setup using the same KRaft configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore Kafka Streams or Apache Flink&lt;/strong&gt; — for in-stream processing (filtering, aggregating, joining streams) without writing a separate consumer application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale to multi-broker&lt;/strong&gt; — repeat the installation on additional nodes, adjust &lt;code&gt;broker.id&lt;/code&gt; and &lt;code&gt;controller.quorum.voters&lt;/code&gt;, and distribute your topics' partitions across the cluster for fault tolerance and horizontal throughput scaling.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kafka</category>
      <category>howto</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>How to Install VMware ESXi: Step-by-Step Bare-Metal Setup Guide</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:45:55 +0000</pubDate>
      <link>https://dev.to/ramansah/how-to-install-vmware-esxi-step-by-step-bare-metal-setup-guide-159c</link>
      <guid>https://dev.to/ramansah/how-to-install-vmware-esxi-step-by-step-bare-metal-setup-guide-159c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://bckinfo.com/how-to-install-vmware-esxi-bare-metal/" rel="noopener noreferrer"&gt;bckinfo.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  How to Install VMware ESXi: Step-by-Step Bare-Metal Setup Guide
&lt;/h1&gt;

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

&lt;ol&gt;
&lt;li&gt;ESXi vs. VMware Workstation: Which One Do You Need&lt;/li&gt;
&lt;li&gt;Hardware Compatibility Check&lt;/li&gt;
&lt;li&gt;Downloading the ESXi Installer&lt;/li&gt;
&lt;li&gt;Creating a Bootable USB Installer&lt;/li&gt;
&lt;li&gt;BIOS/UEFI Preparation&lt;/li&gt;
&lt;li&gt;Installing ESXi: Step by Step&lt;/li&gt;
&lt;li&gt;Configuring the Management Network&lt;/li&gt;
&lt;li&gt;Accessing the vSphere Host Client&lt;/li&gt;
&lt;li&gt;Creating Your First Virtual Machine&lt;/li&gt;
&lt;li&gt;Post-Installation Checklist&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Closing Notes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've read our &lt;a href="https://bckinfo.com/vmware-virtualization-guide/" rel="noopener noreferrer"&gt;complete guide to VMware virtualization&lt;/a&gt;, you already know ESXi is the bare-metal hypervisor underneath vSphere. This guide is the hands-on counterpart — installing ESXi directly on physical server hardware, from hardware compatibility checks through booting your first virtual machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  ESXi vs. VMware Workstation: Which One Do You Need
&lt;/h2&gt;

&lt;p&gt;Before starting, it's worth confirming you actually want ESXi and not VMware Workstation. They solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VMware Workstation&lt;/strong&gt; is a Type-2 hypervisor — it installs &lt;em&gt;on top of&lt;/em&gt; an existing OS (Windows, Linux, macOS via Fusion). Good for running a VM or two on a laptop or desktop you also use for everything else. If that's your case, our guide on &lt;a href="https://bckinfo.com/install-vmware-on-centos-stream10/" rel="noopener noreferrer"&gt;installing VMware Workstation on CentOS Stream 10&lt;/a&gt; is the right starting point instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESXi&lt;/strong&gt; is a Type-1, bare-metal hypervisor — it installs directly on the hardware with no host OS underneath it. This is the right choice for a dedicated server running multiple VMs, a home lab, or anything that needs to scale beyond "a VM running alongside my desktop."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of this guide assumes you're installing on dedicated hardware that won't run anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware Compatibility Check
&lt;/h2&gt;

&lt;p&gt;This is the step most worth not skipping. ESXi has a defined Hardware Compatibility List (HCL), and installing on unlisted hardware is the single biggest source of installation failures and post-install driver issues.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check your exact server model and component list (NIC, storage controller) against VMware's compatibility guide.&lt;/li&gt;
&lt;li&gt;Minimum baseline: a 64-bit x86 CPU with hardware virtualization support (Intel VT-x or AMD-V) enabled in firmware, at least two CPU cores, and 4GB of RAM (8GB+ realistic for anything beyond testing).&lt;/li&gt;
&lt;li&gt;Server vendors (Dell, HPE, Lenovo) often publish &lt;strong&gt;custom ESXi ISO images&lt;/strong&gt; bundled with the correct drivers for their hardware — these are worth using instead of the generic VMware ISO if your hardware vendor provides one, since missing storage or NIC drivers are a common post-install headache otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Downloading the ESXi Installer
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create or log in to a VMware/Broadcom account through the official downloads portal.&lt;/li&gt;
&lt;li&gt;Locate the ESXi ISO matching the version you intend to run.&lt;/li&gt;
&lt;li&gt;If your hardware vendor offers a customized ISO with bundled drivers, prefer that over the generic image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A licensing note: VMware's licensing terms and the availability of any free tier have changed multiple times since Broadcom's acquisition of VMware. Check the current licensing terms on the official Broadcom site before committing to a deployment plan, rather than relying on older guides (including this one) for pricing specifics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Bootable USB Installer
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Get a USB drive with at least 8GB of capacity — it will be fully erased.&lt;/li&gt;
&lt;li&gt;Use a tool like Rufus (Windows) or &lt;code&gt;dd&lt;/code&gt;/Etcher (Linux/macOS) to write the ESXi ISO to the drive.&lt;/li&gt;
&lt;li&gt;Select the ESXi ISO as the source image and the USB drive as the target, then write the image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're installing via IPMI/iDRAC/iLO remote management instead of physical USB, you can mount the ISO directly as a virtual CD-ROM through the server's remote console — skipping the physical media step entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  BIOS/UEFI Preparation
&lt;/h2&gt;

&lt;p&gt;Before booting the installer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter BIOS/UEFI setup (commonly &lt;code&gt;F2&lt;/code&gt;, &lt;code&gt;Del&lt;/code&gt;, or &lt;code&gt;F12&lt;/code&gt; depending on the vendor).&lt;/li&gt;
&lt;li&gt;Confirm hardware virtualization (Intel VT-x / AMD-V) is &lt;strong&gt;enabled&lt;/strong&gt; — some servers ship with it disabled by default.&lt;/li&gt;
&lt;li&gt;Set the USB drive (or virtual CD-ROM, if using IPMI) as the primary boot device.&lt;/li&gt;
&lt;li&gt;If the target disk currently has RAID configured and you intend to change it, configure RAID &lt;strong&gt;before&lt;/strong&gt; installation — changing it afterward means reinstalling.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Installing ESXi: Step by Step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Boot from the installer media. The ESXi installer loads and displays detected CPU and memory.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Enter&lt;/strong&gt; to begin installation.&lt;/li&gt;
&lt;li&gt;Accept the End User License Agreement (&lt;strong&gt;F11&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Wait for device scanning, then select the target disk for installation. This will create a datastore on that disk usable for VM storage.&lt;/li&gt;
&lt;li&gt;Choose your keyboard layout.&lt;/li&gt;
&lt;li&gt;Set the &lt;strong&gt;root password&lt;/strong&gt; — this is the single most important credential on this host; store it in a password manager, not a sticky note.&lt;/li&gt;
&lt;li&gt;Confirm installation with &lt;strong&gt;F11&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Installation typically completes in a few minutes — it's a lean, minimal install by design.&lt;/li&gt;
&lt;li&gt;Remove the installation media when prompted, then reboot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configuring the Management Network
&lt;/h2&gt;

&lt;p&gt;After reboot, you're at the Direct Console User Interface (DCUI) — ESXi's text-based local console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;F2&lt;/strong&gt; and log in as &lt;code&gt;root&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Configure Management Network&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the network adapter to use for management traffic.&lt;/li&gt;
&lt;li&gt;Switch IPv4 configuration from DHCP to &lt;strong&gt;Static&lt;/strong&gt;, and set the IP address, subnet mask, and default gateway.&lt;/li&gt;
&lt;li&gt;Set primary and alternate DNS servers.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Enter&lt;/strong&gt; to confirm, then &lt;strong&gt;Esc&lt;/strong&gt; to apply and restart the management network.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This static IP is what you'll use to reach the host going forward — DHCP is fine for a quick test but impractical the moment you need to consistently reconnect to manage the host or add it to vCenter later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing the vSphere Host Client
&lt;/h2&gt;

&lt;p&gt;Once the management network is configured, open a browser and navigate to &lt;code&gt;https://&amp;lt;esxi-management-ip&amp;gt;/ui&lt;/code&gt;. This launches the &lt;strong&gt;vSphere Host Client&lt;/strong&gt; — a web interface for managing this single host directly, without needing a separate vCenter Server.&lt;/p&gt;

&lt;p&gt;Log in with &lt;code&gt;root&lt;/code&gt; and the password set during installation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Your First Virtual Machine
&lt;/h2&gt;

&lt;p&gt;From the Host Client:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Virtual Machines&lt;/strong&gt; in the left navigation, then &lt;strong&gt;Create / Register VM&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create a new virtual machine&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name the VM and select a guest OS family and version — this affects the default hardware compatibility settings ESXi chooses.&lt;/li&gt;
&lt;li&gt;Select the datastore created during installation.&lt;/li&gt;
&lt;li&gt;Configure CPU, memory, and disk size for the VM.&lt;/li&gt;
&lt;li&gt;Attach an installation ISO (uploaded to the datastore beforehand, or mounted via a remote datastore/content library) as the virtual CD-ROM.&lt;/li&gt;
&lt;li&gt;Power on the VM and proceed with the guest OS installation as you normally would.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;A few things worth doing immediately after a fresh install, before this host runs anything important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set NTP.&lt;/strong&gt; Time drift between the hypervisor and guest VMs causes subtle, hard-to-diagnose issues — configure NTP under host settings before anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable lockdown mode&lt;/strong&gt; if this host will be managed exclusively through vCenter — it restricts direct root login to the host once vCenter is the primary management point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure a separate VM network&lt;/strong&gt; distinct from the management network — mixing management and VM traffic on the same vSwitch is a common security and performance oversight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take a baseline snapshot strategy decision early.&lt;/strong&gt; ESXi snapshots are not backups — plan your actual backup approach for VMs running on this host now, not after something breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Installer doesn't detect the network adapter or storage controller&lt;/td&gt;
&lt;td&gt;Hardware not on the HCL, missing driver in generic ISO&lt;/td&gt;
&lt;td&gt;Use the vendor's customized ISO with bundled drivers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server won't boot from USB&lt;/td&gt;
&lt;td&gt;Boot order not set, or USB not written correctly&lt;/td&gt;
&lt;td&gt;Re-verify BIOS boot order; rewrite the USB with Rufus/Etcher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't reach the Host Client after setup&lt;/td&gt;
&lt;td&gt;Management network still on DHCP and IP changed, or wrong subnet/gateway&lt;/td&gt;
&lt;td&gt;Re-check via DCUI (&lt;code&gt;F2&lt;/code&gt;) and confirm static IP settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM won't power on — "insufficient resources"&lt;/td&gt;
&lt;td&gt;Host RAM/CPU over-allocated to existing VMs&lt;/td&gt;
&lt;td&gt;Reduce other VMs' reservations or add host resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware virtualization not available to nested VMs&lt;/td&gt;
&lt;td&gt;VT-x/AMD-V not enabled in BIOS&lt;/td&gt;
&lt;td&gt;Re-enter BIOS and enable virtualization extensions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;Installing ESXi itself is the easy part — it's a deliberately minimal, fast install. The steps that actually determine whether the host runs well in the long run are the ones before and after: confirming hardware compatibility up front, and getting NTP, network segmentation, and a real backup plan in place before the host starts running anything you'd mind losing.&lt;/p&gt;

&lt;p&gt;If your use case turns out to be simpler than a dedicated bare-metal host — just a VM or two alongside your existing desktop OS — our guide on &lt;a href="https://bckinfo.com/install-vmware-on-centos-stream10/" rel="noopener noreferrer"&gt;installing VMware Workstation on CentOS Stream 10&lt;/a&gt; covers that lighter-weight path instead.&lt;/p&gt;

</description>
      <category>vmware</category>
      <category>virtualization</category>
      <category>sysadmin</category>
      <category>devops</category>
    </item>
    <item>
      <title>PowerShell Automation Scripts: Practical Examples for IT Sysadmins</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:38:33 +0000</pubDate>
      <link>https://dev.to/ramansah/powershell-automation-scripts-practical-examples-for-it-sysadmins-35ko</link>
      <guid>https://dev.to/ramansah/powershell-automation-scripts-practical-examples-for-it-sysadmins-35ko</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://bckinfo.com/powershell-automation-scripts-examples-sysadmins/" rel="noopener noreferrer"&gt;bckinfo.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  PowerShell Automation Scripts: Practical Examples for IT Sysadmins
&lt;/h1&gt;

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

&lt;ol&gt;
&lt;li&gt;What "Automation" Actually Requires&lt;/li&gt;
&lt;li&gt;Foundation: Scheduling a Script with Task Scheduler&lt;/li&gt;
&lt;li&gt;Bulk User Creation from a CSV File&lt;/li&gt;
&lt;li&gt;Automated Backup Script on a Schedule&lt;/li&gt;
&lt;li&gt;Event Log Monitoring and Alerting&lt;/li&gt;
&lt;li&gt;Service Health Check and Auto-Restart&lt;/li&gt;
&lt;li&gt;Secure Credential Handling for Unattended Scripts&lt;/li&gt;
&lt;li&gt;Error Handling and Logging Every Script Needs&lt;/li&gt;
&lt;li&gt;Common Pitfalls&lt;/li&gt;
&lt;li&gt;Where to Go Next&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Knowing PowerShell cmdlets is one skill. Turning them into something that runs unattended, every day, without you babysitting it, is a different one. This guide skips the conceptual overview and goes straight to the scripts sysadmins actually run in production — scheduled tasks, bulk provisioning, backups, monitoring, and the credential-handling and error-handling patterns that make a script safe to leave running while you're not watching it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Automation" Actually Requires
&lt;/h2&gt;

&lt;p&gt;A script that works when you run it manually isn't automated yet. Real automation needs four things working together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A trigger&lt;/strong&gt; — something that starts the script without you (a schedule, an event, a webhook).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — the script needs to fail safely and tell you when it does, since no one is watching it run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential handling&lt;/strong&gt; — unattended scripts can't prompt for a password, so credentials need a secure non-interactive path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt; — a record of what happened, because by the time you notice something went wrong, the script already ran and finished.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every example below builds on this same structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Foundation: Scheduling a Script with Task Scheduler
&lt;/h2&gt;

&lt;p&gt;Almost every automation pattern in this guide eventually needs a trigger, and on Windows that's usually Task Scheduler — configured through PowerShell instead of the GUI, so the setup itself is repeatable and scriptable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create-ScheduledScript.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Execute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PowerShell.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Argument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\DailyMaintenance.ps1"'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$trigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskTrigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Daily&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-At&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3:00AM"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskSettingsSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-StartWhenAvailable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-DontStopOnIdleEnd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-RestartCount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-RestartInterval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-TimeSpan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Minutes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Register-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DailyMaintenance"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Trigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$trigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SYSTEM"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-RunLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Highest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Runs daily maintenance script at 3 AM"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details that matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-NoProfile&lt;/code&gt;&lt;/strong&gt; skips loading the PowerShell profile, which avoids the task silently picking up unrelated customizations from a user profile that may not even be loaded under &lt;code&gt;SYSTEM&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-ExecutionPolicy Bypass&lt;/code&gt;&lt;/strong&gt; applies only to this single invocation — it doesn't change the system-wide execution policy, which is the safer way to run a signed/trusted script without weakening policy everywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RestartCount&lt;/code&gt; / &lt;code&gt;RestartInterval&lt;/code&gt;&lt;/strong&gt; give the task a retry budget if it fails — useful for scripts that depend on a network resource that might be briefly unavailable at 3 AM.&lt;/li&gt;
&lt;li&gt;Running as &lt;strong&gt;&lt;code&gt;SYSTEM&lt;/code&gt;&lt;/strong&gt; is convenient but has no network identity; if the script needs to reach another server with Windows authentication, a dedicated service account is usually the better choice — see the credential handling section below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bulk User Creation from a CSV File
&lt;/h2&gt;

&lt;p&gt;A common onboarding task: HR drops a CSV, and dozens of Active Directory accounts need to exist by morning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# New-UsersFromCsv.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$CsvPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Logs\UserCreation.log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Import-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ActiveDirectory&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Import-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CsvPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertTo-SecureString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitialPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AsPlainText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="n"&gt;New-ADUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-GivenName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Surname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-SamAccountName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-UserPrincipalName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Username&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;@company.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OUPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-AccountPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-ChangePasswordAtLogon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="n"&gt;Add-ADGroupMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PSCustomObject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PSCustomObject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Exception&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Export-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoTypeInformation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Append&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$failed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Warning&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$failed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; account(s) failed. See &lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="s2"&gt; for details."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected CSV columns: &lt;code&gt;FirstName, LastName, Username, InitialPassword, OUPath, GroupName&lt;/code&gt;. Processing each row inside its own &lt;code&gt;try/catch&lt;/code&gt; is the detail that matters most here — without it, one bad row (a duplicate username, an invalid OU path) stops the entire batch instead of just logging that one failure and continuing with the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Backup Script on a Schedule
&lt;/h2&gt;

&lt;p&gt;A straightforward file backup with timestamped archives and automatic cleanup of old backups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Backup-Directory.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$SourcePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\ImportantData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$BackupRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Backups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$RetentionDays&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yyyyMMdd-HHmmss"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$BackupRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup-&lt;/span&gt;&lt;span class="nv"&gt;$timestamp&lt;/span&gt;&lt;span class="s2"&gt;.zip"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Compress-Archive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SourcePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DestinationPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup created: &lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# Remove backups older than the retention period&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$BackupRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup-*.zip"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastWriteTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$RetentionDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cleanup complete: removed backups older than &lt;/span&gt;&lt;span class="nv"&gt;$RetentionDays&lt;/span&gt;&lt;span class="s2"&gt; days"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup failed: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern — compress, timestamp, then prune anything past a retention window — is intentionally simple, but it's the same shape used for database dumps. If you're backing up MongoDB or Redis running in Docker rather than flat files, the compression step changes but the retention and scheduling logic stays identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Log Monitoring and Alerting
&lt;/h2&gt;

&lt;p&gt;Checking for specific security or system events and sending a notification when they appear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Monitor-EventLog.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$LogName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="bp"&gt;$Event&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4625&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# Failed logon&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$LookbackMinutes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$AlertEmail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"soc-team@company.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$startTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$LookbackMinutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilterHashtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;LogName&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LogName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$Event&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;StartTime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$startTime&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$summary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Group-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Detected &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt; failed logon attempt(s) in the last &lt;/span&gt;&lt;span class="nv"&gt;$LookbackMinutes&lt;/span&gt;&lt;span class="s2"&gt; minutes.&lt;/span&gt;&lt;span class="se"&gt;`n`n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$summary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-Table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AutoSize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="n"&gt;Send-MailMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-To&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$AlertEmail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-From&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alerts@company.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security Alert: &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt; Failed Logons Detected"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SmtpServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smtp.company.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Get-WinEvent&lt;/code&gt; with &lt;code&gt;-FilterHashtable&lt;/code&gt; is significantly faster than the older &lt;code&gt;Get-EventLog&lt;/code&gt; cmdlet on large logs, since the filtering happens at the provider level instead of pulling every event into memory first. This script is built to run every 15 minutes via Task Scheduler, checking only the window since its last run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Health Check and Auto-Restart
&lt;/h2&gt;

&lt;p&gt;For services that occasionally stop and need to be brought back up without a human noticing first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Watch-CriticalServices.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]]&lt;/span&gt;&lt;span class="nv"&gt;$ServiceNames&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s2"&gt;"MSSQLSERVER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"W3SVC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyAppService"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Logs\ServiceWatchdog.log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ServiceNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - WARNING - Service '&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;' not found"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Append&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;continue&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Running'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Start-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - INFO - Restarted service '&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Append&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - ERROR - Failed to restart '&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;': &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Append&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LogPath&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this every five minutes for a lightweight watchdog. For anything beyond a handful of services, dedicated monitoring tools are a better fit — this script is meant for small environments where standing up a full monitoring stack isn't justified yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure Credential Handling for Unattended Scripts
&lt;/h2&gt;

&lt;p&gt;Hardcoding a password in a &lt;code&gt;.ps1&lt;/code&gt; file is the single most common security mistake in automation scripts — it sits in plain text, gets picked up by version control if you're not careful, and is readable by anyone with file access. PowerShell's &lt;code&gt;SecretManagement&lt;/code&gt; module solves this properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-time setup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Install-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.PowerShell.SecretManagement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CurrentUser&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Install-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.PowerShell.SecretStore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CurrentUser&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Register-SecretVault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LocalVault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ModuleName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.PowerShell.SecretStore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DefaultVault&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Store a credential once&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-Secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ServiceAccountPassword"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Read-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AsSecureString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enter password"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, inside any automation script, retrieve it without ever typing the password again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$securePassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ServiceAccountPassword"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$credential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Management.Automation.PSCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DOMAIN\svc-automation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$securePassword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Invoke-Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ComputerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server01"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Credential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$credential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ScriptBlock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyAppService"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For environments already using a centralized secrets manager — Azure Key Vault is the common case — &lt;code&gt;SecretManagement&lt;/code&gt; also supports vault extensions that pull from there instead of a local store, which is the better option once more than one machine needs access to the same credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling and Logging Every Script Needs
&lt;/h2&gt;

&lt;p&gt;Every script above already uses this pattern, but it's worth calling out on its own: &lt;strong&gt;&lt;code&gt;try/catch&lt;/code&gt; with &lt;code&gt;-ErrorAction Stop&lt;/code&gt;&lt;/strong&gt;, not the default error behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NonexistentService"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Caught error: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# Log it, send an alert, or exit with a non-zero code so a calling process knows it failed&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, most cmdlet errors are &lt;strong&gt;non-terminating&lt;/strong&gt; — PowerShell logs the error and keeps going to the next line, which means a &lt;code&gt;catch&lt;/code&gt; block never triggers unless you explicitly set &lt;code&gt;-ErrorAction Stop&lt;/code&gt; on the command that might fail. This is the most common reason a script's error handling silently does nothing: the &lt;code&gt;try/catch&lt;/code&gt; is there, but the error inside it was never terminating in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pitfall&lt;/th&gt;
&lt;th&gt;Why It Bites&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Script works manually, fails as scheduled task&lt;/td&gt;
&lt;td&gt;Runs as a different user/profile, no console for prompts&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;-NoProfile&lt;/code&gt;, avoid &lt;code&gt;Read-Host&lt;/code&gt;, test by running as the task's actual account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;try/catch&lt;/code&gt; never catches anything&lt;/td&gt;
&lt;td&gt;Cmdlet error was non-terminating&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;-ErrorAction Stop&lt;/code&gt; to the command inside the &lt;code&gt;try&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded password in script file&lt;/td&gt;
&lt;td&gt;Plain-text credential exposure&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;SecretManagement&lt;/code&gt;/&lt;code&gt;SecretStore&lt;/code&gt; or a vault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task runs but script silently does nothing&lt;/td&gt;
&lt;td&gt;Execution policy blocking the script&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;-ExecutionPolicy Bypass&lt;/code&gt; on the scheduled action, not a system-wide policy change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script breaks after a Windows update&lt;/td&gt;
&lt;td&gt;Module path or cmdlet behavior changed between PowerShell versions&lt;/td&gt;
&lt;td&gt;Pin module versions, test on PowerShell 7+ before relying on Core-only behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;These scripts assume familiarity with PowerShell's basics — cmdlets, the object pipeline, and execution policy. If any of that needs a refresher first, our &lt;a href="https://bckinfo.com/powershell-a-complete-guide-to-automation-management/" rel="noopener noreferrer"&gt;PowerShell: A Complete Guide to Automation and Task Management for IT Professionals&lt;/a&gt; guide covers the foundational concepts this article builds directly on top of.&lt;/p&gt;

&lt;p&gt;From here, the same trigger-plus-error-handling-plus-credential pattern extends to almost anything you need to automate: certificate renewal checks, disk space alerts, bulk software deployment, or cloud resource provisioning with Azure PowerShell. Start with whichever task currently eats the most manual time, automate that one first, and the pattern gets easier to reuse on the next one.&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>automation</category>
      <category>sysadmin</category>
      <category>scripting</category>
    </item>
    <item>
      <title>MongoDB with Docker Compose: Authentication, Replica Set, and Production-Ready Setup</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:32:27 +0000</pubDate>
      <link>https://dev.to/ramansah/mongodb-with-docker-compose-authentication-replica-set-and-production-ready-setup-3d8a</link>
      <guid>https://dev.to/ramansah/mongodb-with-docker-compose-authentication-replica-set-and-production-ready-setup-3d8a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://bckinfo.com/mongodb-docker-compose-replica-set-authentication/" rel="noopener noreferrer"&gt;bckinfo.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  MongoDB with Docker Compose: Authentication, Replica Set, and Production-Ready Setup
&lt;/h1&gt;

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

&lt;ol&gt;
&lt;li&gt;Why a Standalone Container Isn't Enough&lt;/li&gt;
&lt;li&gt;Basic Setup: Single Container with Persistence&lt;/li&gt;
&lt;li&gt;Enabling Authentication&lt;/li&gt;
&lt;li&gt;Single-Node Replica Set for Transactions&lt;/li&gt;
&lt;li&gt;Keyfile Authentication for Replica Sets&lt;/li&gt;
&lt;li&gt;Full 3-Node Replica Set for High Availability&lt;/li&gt;
&lt;li&gt;Health Checks and Auto-Initialization&lt;/li&gt;
&lt;li&gt;Connection String Patterns&lt;/li&gt;
&lt;li&gt;Backup Considerations&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Closing Notes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MongoDB is straightforward to start in Docker — pull the image, mount a volume, and you have a working database in under a minute. The complexity shows up the moment your application needs something a single standalone instance can't provide: multi-document transactions, change streams, or basic high availability. All three require a &lt;strong&gt;replica set&lt;/strong&gt;, and setting one up correctly inside Docker Compose has a few sharp edges worth knowing before you hit them in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Standalone Container Isn't Enough
&lt;/h2&gt;

&lt;p&gt;A default MongoDB container — just &lt;code&gt;image: mongo&lt;/code&gt; with no extra flags — runs as a standalone instance. That's fine for simple CRUD prototyping, but it silently blocks two things many applications eventually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-document transactions&lt;/strong&gt; — MongoDB only supports ACID transactions across a replica set, even a single-node one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change streams&lt;/strong&gt; — real-time data pipelines, cache invalidation, or event-driven architectures built on &lt;code&gt;watch()&lt;/code&gt; require replication to function at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever seen an ORM or driver (Prisma is a common example) throw an error demanding a replica set, this is why. The fix isn't complicated, but it does mean a standalone container is rarely the right default for anything beyond a quick prototype.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Setup: Single Container with Persistence
&lt;/h2&gt;

&lt;p&gt;Start with the foundation — a container that keeps its data after a restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongodb&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;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;27017:27017"&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;mongo-data:/data/db&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official MongoDB image stores its database files at &lt;code&gt;/data/db&lt;/code&gt;. As with Redis, a &lt;strong&gt;named volume&lt;/strong&gt; is preferable to a bind mount here — Docker manages its lifecycle, and it stays portable across hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Authentication
&lt;/h2&gt;

&lt;p&gt;Authentication is &lt;strong&gt;not enabled by default&lt;/strong&gt; on the official MongoDB image. The first time you run it, set a root user through environment variables:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongodb&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGO_INITDB_ROOT_USERNAME&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;MONGO_INITDB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MONGO_ROOT_PASSWORD}&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;27017:27017"&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;mongo-data:/data/db&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These variables only take effect &lt;strong&gt;on first initialization&lt;/strong&gt; of an empty data directory — changing them later won't update an existing user. Keep the password in a &lt;code&gt;.env&lt;/code&gt; file excluded from version control rather than hardcoded in the compose file.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mongosh &lt;span class="nt"&gt;--port&lt;/span&gt; 27017 &lt;span class="nt"&gt;--username&lt;/span&gt; root &lt;span class="nt"&gt;--password&lt;/span&gt; &lt;span class="nt"&gt;--authenticationDatabase&lt;/span&gt; admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Single-Node Replica Set for Transactions
&lt;/h2&gt;

&lt;p&gt;The fastest way to unlock transactions and change streams in development is converting your single container into a &lt;strong&gt;one-member replica set&lt;/strong&gt; — no extra containers required:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongodb&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;command&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;--replSet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--bind_ip_all"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGO_INITDB_ROOT_USERNAME&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;MONGO_INITDB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MONGO_ROOT_PASSWORD}&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;27017:27017"&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;mongo-data:/data/db&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'127.0.0.1:27017'}]}) }"&lt;/span&gt;
        &lt;span class="s"&gt;| mongosh --port 27017 -u root -p ${MONGO_ROOT_PASSWORD} --authenticationDatabase admin --quiet&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&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;10&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The health check does double duty here: it both verifies MongoDB is responsive &lt;strong&gt;and&lt;/strong&gt; initializes the replica set automatically on first boot if it hasn't been configured yet. This is a convenient pattern for local development and staging — for production, see the dedicated initialization service further down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyfile Authentication for Replica Sets
&lt;/h2&gt;

&lt;p&gt;Here's the edge that catches people off guard: &lt;strong&gt;once you combine authentication with replication&lt;/strong&gt;, MongoDB requires internal cluster members to authenticate with each other using a shared keyfile — separate from the root user's password. Without it, replica set members can't establish trust between themselves.&lt;/p&gt;

&lt;p&gt;Generate a keyfile and mount it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 756 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mongo-keyfile
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 mongo-keyfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo&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;mongo:7.0&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;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--replSet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--keyFile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/mongo-keyfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--bind_ip_all"&lt;/span&gt;&lt;span class="pi"&gt;]&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;mongo-data:/data/db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mongo-keyfile:/etc/mongo-keyfile:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MONGO_INITDB_ROOT_USERNAME&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;MONGO_INITDB_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MONGO_ROOT_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common permissions trap: the official image runs as the &lt;code&gt;mongodb&lt;/code&gt; user internally, and a keyfile mounted with the wrong ownership or overly permissive mode (anything looser than &lt;code&gt;400&lt;/code&gt;) will be silently rejected at startup with a generic authentication error. If replication fails to establish after adding a keyfile, check permissions first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full 3-Node Replica Set for High Availability
&lt;/h2&gt;

&lt;p&gt;For an environment that actually needs failover — not just transaction support — run three MongoDB instances on a shared network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo1&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo1&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo1&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;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mongod"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--replSet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--keyFile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/mongo-keyfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--bind_ip_all"&lt;/span&gt;&lt;span class="pi"&gt;]&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;mongo1-data:/data/db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mongo-keyfile:/etc/mongo-keyfile:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-net&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mongosh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--eval"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db.adminCommand('ping').ok"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--quiet"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&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;5&lt;/span&gt;

  &lt;span class="na"&gt;mongo2&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo2&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo2&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;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mongod"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--replSet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--keyFile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/mongo-keyfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--bind_ip_all"&lt;/span&gt;&lt;span class="pi"&gt;]&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;mongo2-data:/data/db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mongo-keyfile:/etc/mongo-keyfile:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-net&lt;/span&gt;

  &lt;span class="na"&gt;mongo3&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo3&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo3&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;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mongod"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--replSet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rs0"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--keyFile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/mongo-keyfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--bind_ip_all"&lt;/span&gt;&lt;span class="pi"&gt;]&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;mongo3-data:/data/db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mongo-keyfile:/etc/mongo-keyfile:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-net&lt;/span&gt;

  &lt;span class="na"&gt;mongo-init&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;mongo:7.0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mongo1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-net&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&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;bash -c "&lt;/span&gt;
        &lt;span class="s"&gt;mongosh --host mongo1 -u root -p ${MONGO_ROOT_PASSWORD} --authenticationDatabase admin --eval '&lt;/span&gt;
        &lt;span class="s"&gt;rs.initiate({&lt;/span&gt;
          &lt;span class="s"&gt;_id: \"rs0\",&lt;/span&gt;
          &lt;span class="s"&gt;members: [&lt;/span&gt;
            &lt;span class="s"&gt;{ _id: 0, host: \"mongo1:27017\" },&lt;/span&gt;
            &lt;span class="s"&gt;{ _id: 1, host: \"mongo2:27017\" },&lt;/span&gt;
            &lt;span class="s"&gt;{ _id: 2, host: \"mongo3:27017\" }&lt;/span&gt;
          &lt;span class="s"&gt;]&lt;/span&gt;
        &lt;span class="s"&gt;})'&lt;/span&gt;
      &lt;span class="s"&gt;"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo1-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo2-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo3-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each node uses its &lt;strong&gt;service name as hostname&lt;/strong&gt; within the &lt;code&gt;mongo-net&lt;/code&gt; network — &lt;code&gt;mongo1&lt;/code&gt;, &lt;code&gt;mongo2&lt;/code&gt;, &lt;code&gt;mongo3&lt;/code&gt; — and the replica set configuration references those names directly rather than IP addresses, which would break the moment containers restart and get reassigned. This is the same hostname-based service discovery pattern used in any multi-container Compose stack, and it's worth pairing with the network segmentation approach in our Docker Container Security Best Practices guide if this stack is internet-facing in any way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health Checks and Auto-Initialization
&lt;/h2&gt;

&lt;p&gt;A dedicated &lt;code&gt;mongo-init&lt;/code&gt; service — as shown above — is the cleanest way to handle replica set initialization in a repeatable, automated way, rather than running &lt;code&gt;rs.initiate()&lt;/code&gt; manually after every fresh deployment. The pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;mongo-init&lt;/code&gt; waits for &lt;code&gt;mongo1&lt;/code&gt; to report healthy via &lt;code&gt;depends_on.condition: service_healthy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It runs &lt;code&gt;rs.initiate()&lt;/code&gt; exactly once, targeting all three members.&lt;/li&gt;
&lt;li&gt;The container exits after running — it's a one-shot initialization job, not a long-running service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you re-run &lt;code&gt;docker compose up&lt;/code&gt; on an already-initialized cluster, &lt;code&gt;rs.initiate()&lt;/code&gt; will simply fail harmlessly since the replica set already exists — safe to leave in place for idempotent deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection String Patterns
&lt;/h2&gt;

&lt;p&gt;Once authentication and replication are both in place, your application's connection string needs two extra parameters compared to a standalone setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mongodb://appuser:apppassword@mongo1:27017,mongo2:27017,mongo3:27017/myapp?replicaSet=rs0&amp;amp;authSource=admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple hosts&lt;/strong&gt; in the connection string let the driver discover the current primary even after a failover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;replicaSet=rs0&lt;/code&gt;&lt;/strong&gt; must match the &lt;code&gt;_id&lt;/code&gt; used in &lt;code&gt;rs.initiate()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;authSource=admin&lt;/code&gt;&lt;/strong&gt; tells the driver which database the user's credentials are stored in — almost always &lt;code&gt;admin&lt;/code&gt; for a root-style user, even when connecting to a different application database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local development against the single-node setup, the equivalent is simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mongodb://root:password@localhost:27017/?replicaSet=rs0&amp;amp;authSource=admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Backup Considerations
&lt;/h2&gt;

&lt;p&gt;A replica set doesn't replace backups — replication protects against node failure, not against an accidental &lt;code&gt;deleteMany()&lt;/code&gt; or application bug that corrupts data across all members simultaneously. The backup approach is the same regardless of whether you're running standalone or a replica set: &lt;code&gt;mongodump&lt;/code&gt; against a secondary node to avoid load on the primary, archived on a schedule outside the container. Our existing MongoDB Backup guide covers the full scripted approach, including restore testing — worth pairing with this setup once your replica set is running.&lt;/p&gt;

&lt;p&gt;If your stack also runs Redis alongside MongoDB, the volume-and-backup discipline described in our Redis with Docker Compose guide follows the same underlying pattern — named volumes, scheduled snapshots, and a tested restore procedure for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Driver throws "Transaction numbers are only allowed on a replica set"&lt;/td&gt;
&lt;td&gt;Standalone instance, no replication configured&lt;/td&gt;
&lt;td&gt;Convert to a (single-node or multi-node) replica set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replica set members can't authenticate with each other&lt;/td&gt;
&lt;td&gt;Missing or misconfigured keyfile&lt;/td&gt;
&lt;td&gt;Generate a keyfile, mount with &lt;code&gt;chmod 400&lt;/code&gt;, verify ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rs.initiate()&lt;/code&gt; hangs or times out&lt;/td&gt;
&lt;td&gt;Members can't resolve each other's hostnames&lt;/td&gt;
&lt;td&gt;Confirm all nodes share the same Docker network and use service names, not &lt;code&gt;localhost&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App can't find primary after failover&lt;/td&gt;
&lt;td&gt;Connection string only lists one host&lt;/td&gt;
&lt;td&gt;List all replica set members in the connection string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MONGO_INITDB_ROOT_USERNAME&lt;/code&gt; has no effect&lt;/td&gt;
&lt;td&gt;Variables only apply on first init of an empty volume&lt;/td&gt;
&lt;td&gt;Remove the volume to reset, or create the user manually via &lt;code&gt;mongosh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;Running MongoDB in Docker Compose is simple for a prototype and only slightly more involved once production requirements show up: authentication from the start, a replica set the moment transactions or change streams enter the picture, a keyfile the moment authentication and replication combine, and health checks that make initialization repeatable instead of a manual one-off step.&lt;/p&gt;

&lt;p&gt;From here, pair this setup with our MongoDB Backup guide for the operational side, and our Redis with Docker Compose guide if your stack runs both databases side by side — the persistence and security patterns carry over directly between the two.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>mongodb</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Redis with Docker Compose: Persistence, Security, and Production-Ready Configuration</title>
      <dc:creator>Ramansah</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:19:52 +0000</pubDate>
      <link>https://dev.to/ramansah/redis-with-docker-compose-persistence-security-and-production-ready-configuration-58jb</link>
      <guid>https://dev.to/ramansah/redis-with-docker-compose-persistence-security-and-production-ready-configuration-58jb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://bckinfo.com/redis-docker-compose-persistence-security/" rel="noopener noreferrer"&gt;bckinfo.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Redis with Docker Compose: Persistence, Security, and Production-Ready Configuration
&lt;/h1&gt;

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

&lt;ol&gt;
&lt;li&gt;Why Redis Containers Lose Data&lt;/li&gt;
&lt;li&gt;RDB vs AOF: Choosing a Persistence Strategy&lt;/li&gt;
&lt;li&gt;Basic Setup with Docker Compose&lt;/li&gt;
&lt;li&gt;Full Persistence Configuration&lt;/li&gt;
&lt;li&gt;Securing Redis in Docker&lt;/li&gt;
&lt;li&gt;Setting Resource Limits&lt;/li&gt;
&lt;li&gt;Redis in a Multi-Service Stack&lt;/li&gt;
&lt;li&gt;Backing Up and Restoring a Redis Volume&lt;/li&gt;
&lt;li&gt;Common Issues and Quick Fixes&lt;/li&gt;
&lt;li&gt;Closing Notes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Redis is one of the most common services to run in Docker — it's fast to spin up, lightweight, and perfect for caching, session storage, and queues. But that same simplicity hides a trap: by default, Redis in Docker stores everything in memory, and the moment a container is removed, &lt;strong&gt;all of that data disappears&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This guide walks through setting up Redis with Docker Compose the right way — covering persistence, authentication, resource limits, and the health checks you need before putting it anywhere near production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Redis Containers Lose Data
&lt;/h2&gt;

&lt;p&gt;A Docker container is meant to be disposable. That's a feature for stateless services, but it's a liability for a database like Redis. If you start a plain Redis container without a mounted volume, here's what happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The container writes its dataset only inside its own writable layer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose down&lt;/code&gt; or &lt;code&gt;docker rm&lt;/code&gt; removes that layer entirely.&lt;/li&gt;
&lt;li&gt;The next time the container starts, Redis initializes with an empty dataset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This single oversight accounts for a large share of "we lost our session data" incidents in small teams running Redis in containers for the first time. The fix is straightforward once you understand the two persistence mechanisms Redis offers.&lt;/p&gt;

&lt;h2&gt;
  
  
  RDB vs AOF: Choosing a Persistence Strategy
&lt;/h2&gt;

&lt;p&gt;Redis supports two persistence models, and production setups typically combine both:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDB (Redis Database snapshots)&lt;/strong&gt;&lt;br&gt;
Point-in-time snapshots of the dataset, saved at intervals you define. Fast to restore, but you can lose any writes that happened after the last snapshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AOF (Append Only File)&lt;/strong&gt;&lt;br&gt;
Every write operation is logged to disk as it happens. Slower to restore on a large dataset, but far safer — with &lt;code&gt;appendfsync everysec&lt;/code&gt;, you lose at most one second of writes.&lt;/p&gt;

&lt;p&gt;For most production workloads, &lt;strong&gt;enable both&lt;/strong&gt;: AOF for durability, RDB for fast snapshot-based backups.&lt;/p&gt;
&lt;h2&gt;
  
  
  Basic Setup with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Start with a minimal but persistent configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&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;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&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;6379:6379"&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;redis-data:/data&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --appendonly yes&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key line here is &lt;code&gt;volumes: - redis-data:/data&lt;/code&gt;. The official Redis image is already configured to write its dataset to &lt;code&gt;/data&lt;/code&gt;, so mounting a named volume there is enough to survive a container removal. Named volumes are preferred over bind mounts for this purpose — they're portable across hosts and Docker manages their lifecycle for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Persistence Configuration
&lt;/h2&gt;

&lt;p&gt;For more control, mount a custom &lt;code&gt;redis.conf&lt;/code&gt; instead of relying on command-line flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# redis.conf
&lt;/span&gt;
&lt;span class="c"&gt;# Network
&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;
&lt;span class="n"&gt;protected&lt;/span&gt;-&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;

&lt;span class="c"&gt;# Security
&lt;/span&gt;&lt;span class="n"&gt;requirepass&lt;/span&gt; &lt;span class="n"&gt;your_secure_password_here&lt;/span&gt;

&lt;span class="c"&gt;# Memory management
&lt;/span&gt;&lt;span class="n"&gt;maxmemory&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;&lt;span class="n"&gt;mb&lt;/span&gt;
&lt;span class="n"&gt;maxmemory&lt;/span&gt;-&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;allkeys&lt;/span&gt;-&lt;span class="n"&gt;lru&lt;/span&gt;

&lt;span class="c"&gt;# AOF persistence
&lt;/span&gt;&lt;span class="n"&gt;appendonly&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;appendfsync&lt;/span&gt; &lt;span class="n"&gt;everysec&lt;/span&gt;
&lt;span class="n"&gt;auto&lt;/span&gt;-&lt;span class="n"&gt;aof&lt;/span&gt;-&lt;span class="n"&gt;rewrite&lt;/span&gt;-&lt;span class="n"&gt;percentage&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;span class="n"&gt;auto&lt;/span&gt;-&lt;span class="n"&gt;aof&lt;/span&gt;-&lt;span class="n"&gt;rewrite&lt;/span&gt;-&lt;span class="n"&gt;min&lt;/span&gt;-&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="n"&gt;mb&lt;/span&gt;

&lt;span class="c"&gt;# RDB persistence
&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;
&lt;span class="n"&gt;stop&lt;/span&gt;-&lt;span class="n"&gt;writes&lt;/span&gt;-&lt;span class="n"&gt;on&lt;/span&gt;-&lt;span class="n"&gt;bgsave&lt;/span&gt;-&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;rdbcompression&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;rdbchecksum&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;

&lt;span class="c"&gt;# Logging
&lt;/span&gt;&lt;span class="n"&gt;loglevel&lt;/span&gt; &lt;span class="n"&gt;notice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount it in Docker Compose:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&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;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&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;6379:6379"&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;redis-data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./redis.conf:/usr/local/etc/redis/redis.conf:ro&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server /usr/local/etc/redis/redis.conf&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mounting the config file as read-only (&lt;code&gt;:ro&lt;/code&gt;) prevents Redis itself from accidentally modifying it, and keeps your configuration under version control instead of buried in command-line arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Redis in Docker
&lt;/h2&gt;

&lt;p&gt;Redis has no authentication enabled by default, and it has been a recurring target for cryptomining botnets that scan the internet for exposed instances on port 6379. A few non-negotiable steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always set &lt;code&gt;requirepass&lt;/code&gt;.&lt;/strong&gt; Never run Redis with &lt;code&gt;ALLOW_EMPTY_PASSWORD&lt;/code&gt; outside of a local, throwaway development environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never expose port 6379 directly to the internet.&lt;/strong&gt; If remote access is genuinely needed, put it behind a VPN or SSH tunnel — not a public port mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable dangerous commands&lt;/strong&gt; in production, such as &lt;code&gt;FLUSHALL&lt;/code&gt;, &lt;code&gt;FLUSHDB&lt;/code&gt;, and &lt;code&gt;CONFIG&lt;/code&gt;, using &lt;code&gt;rename-command&lt;/code&gt; in &lt;code&gt;redis.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run as a non-root user.&lt;/strong&gt; The official and Bitnami images already drop privileges by default — don't override this with &lt;code&gt;SKIP_DROP_PRIVS&lt;/code&gt; unless you have a specific reason to.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same defense-in-depth mindset covered in our Docker Container Security Best Practices guide — Redis just adds a database-specific layer to those general container hardening principles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Resource Limits
&lt;/h2&gt;

&lt;p&gt;An unbounded Redis instance can consume all available host memory under heavy load, taking down other services on the same machine. Set explicit limits:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&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;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1G&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256M&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --maxmemory 800mb --maxmemory-policy allkeys-lru&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the two limits work at different layers: &lt;code&gt;deploy.resources.limits.memory&lt;/code&gt; is enforced by Docker itself (the container gets OOM-killed if it crosses this), while &lt;code&gt;maxmemory&lt;/code&gt; inside Redis tells Redis to start evicting keys &lt;em&gt;before&lt;/em&gt; that happens — using &lt;code&gt;allkeys-lru&lt;/code&gt; as a sensible general-purpose eviction policy for caching workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis in a Multi-Service Stack
&lt;/h2&gt;

&lt;p&gt;When Redis is shared by an API and a background worker, startup order matters. A worker that connects before Redis is ready will crash-loop. Use a health check with &lt;code&gt;condition: service_healthy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&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;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}&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;redis-data:/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-a"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDIS_PASSWORD}"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&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;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&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;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;api&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="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://:${REDIS_PASSWORD}@redis:6379/0&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;worker&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="s"&gt;./worker&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://:${REDIS_PASSWORD}@redis:6379/1&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth highlighting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logical databases.&lt;/strong&gt; Redis supports 16 logical databases (0–15) on a single instance. Assigning a different database index per service (&lt;code&gt;/0&lt;/code&gt; for the API, &lt;code&gt;/1&lt;/code&gt; for the worker) avoids key collisions without running multiple Redis containers — useful in smaller setups where running separate instances would be overkill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom bridge network.&lt;/strong&gt; Putting Redis on its own &lt;code&gt;backend&lt;/code&gt; network — separate from any network that's exposed to the outside — limits which containers can even attempt to reach it. This pairs directly with the network segmentation principles in our Docker Network Security guide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backing Up and Restoring a Redis Volume
&lt;/h2&gt;

&lt;p&gt;Even with persistence enabled, you still want backups independent of the running container — protecting against accidental &lt;code&gt;FLUSHALL&lt;/code&gt;, corrupted volumes, or host failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# backup-redis-volume.sh&lt;/span&gt;
&lt;span class="nv"&gt;VOLUME_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"redis_redis-data"&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/backups/redis"&lt;/span&gt;
&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d-%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Trigger a snapshot before backing up&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;redis redis-cli BGSAVE
&lt;span class="nb"&gt;sleep &lt;/span&gt;5

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VOLUME_NAME&lt;/span&gt;&lt;span class="s2"&gt;:/data:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;:/backup"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="nb"&gt;tar &lt;/span&gt;czf &lt;span class="s2"&gt;"/backup/redis-data-&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backup created: &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/redis-data-&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Restore script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# restore-redis-volume.sh&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;VOLUME_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"redis_redis-data"&lt;/span&gt;

docker compose stop redis

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VOLUME_NAME&lt;/span&gt;&lt;span class="s2"&gt;:/data"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;:/backup:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="nb"&gt;tar &lt;/span&gt;xzf &lt;span class="s2"&gt;"/backup/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; /data

docker compose start redis
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Restore complete from &lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same backup-then-archive pattern is used in our MongoDB Backup article — if you're running both databases in the same stack, you can adapt one scheduled cron job to cover both with minimal changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues and Quick Fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data gone after &lt;code&gt;docker compose down&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No volume mounted&lt;/td&gt;
&lt;td&gt;Add a named volume at &lt;code&gt;/data&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WRONGPASS&lt;/code&gt; errors from app&lt;/td&gt;
&lt;td&gt;Password mismatch between &lt;code&gt;redis.conf&lt;/code&gt; and app env var&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;REDIS_PASSWORD&lt;/code&gt; matches in both places&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis container OOM-killed&lt;/td&gt;
&lt;td&gt;No memory limit / no eviction policy&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;deploy.resources.limits.memory&lt;/code&gt; and &lt;code&gt;maxmemory-policy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker crash-loops on startup&lt;/td&gt;
&lt;td&gt;Worker starts before Redis is ready&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;healthcheck&lt;/code&gt; + &lt;code&gt;condition: service_healthy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High latency under load&lt;/td&gt;
&lt;td&gt;Synchronous AOF fsync&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;appendfsync everysec&lt;/code&gt; instead of &lt;code&gt;always&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Closing Notes
&lt;/h2&gt;

&lt;p&gt;Running Redis in Docker Compose is simple to get started with — and just as simple to get wrong if persistence and security are treated as an afterthought. The pattern that holds up well in production is consistent: named volumes for &lt;code&gt;/data&lt;/code&gt;, AOF+RDB persistence enabled together, &lt;code&gt;requirepass&lt;/code&gt; always set, explicit memory limits, and a health check gating any service that depends on Redis being ready.&lt;/p&gt;

&lt;p&gt;From here, if your stack also includes MongoDB or another database alongside Redis, the same volume-and-backup discipline applies — see our MongoDB Backup guide for the database-specific details, and our Docker Network Security guide for locking down how these containers talk to each other.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dockercompose</category>
      <category>redis</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
