<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mahdi Shadi]]></title><description><![CDATA[DevOps Engineer, Linux Lover, Chess Player]]></description><link>https://blog.mahdishadi.me</link><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 20:55:51 GMT</lastBuildDate><atom:link href="https://blog.mahdishadi.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Deploying Jitsi Meet Behind Traefik with Docker Compose (Custom SSL PEM/KEY + Host Authentication)]]></title><description><![CDATA[This guide walks through how we deployed Jitsi Meet using Docker Compose, placed it behind Traefik, terminated TLS using our own certificate (.pem + .key) via Traefik’s File Provider, and enabled Host Authentication using internal auth (Prosody users...]]></description><link>https://blog.mahdishadi.me/deploying-jitsi-meet-behind-traefik-with-docker</link><guid isPermaLink="true">https://blog.mahdishadi.me/deploying-jitsi-meet-behind-traefik-with-docker</guid><category><![CDATA[Docker]]></category><category><![CDATA[jitsi meet]]></category><category><![CDATA[Traefik]]></category><category><![CDATA[Docker compose]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Mon, 16 Feb 2026 07:54:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771228221999/20c51f41-abe4-4fee-b27c-703b0c182e49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This guide walks through how we deployed <strong>Jitsi Meet</strong> using <strong>Docker Compose</strong>, placed it <strong>behind Traefik</strong>, terminated <strong>TLS using our own certificate</strong> (<code>.pem</code> + <code>.key</code>) via Traefik’s <strong>File Provider</strong>, and enabled <strong>Host Authentication</strong> using <strong>internal auth</strong> (Prosody users).</p>
<hr />
<h2 id="heading-1-target-setup-and-architecture">1) Target Setup and Architecture</h2>
<p>Goals:</p>
<ul>
<li><p>Traefik is already listening on <strong>443</strong> (used by other services).</p>
</li>
<li><p>Jitsi must run <strong>behind Traefik</strong> and not take over port 443 directly.</p>
</li>
<li><p>TLS must be served using a <strong>custom certificate</strong> (not Let’s Encrypt).</p>
</li>
<li><p>Enable <strong>Host Authentication</strong> (internal auth), so only authenticated users can host/start meetings (and optionally allow guests to join).</p>
</li>
</ul>
<p>Traffic flow:</p>
<pre><code class="lang-yaml"><span class="hljs-string">Internet</span> <span class="hljs-string">(https://meet.shonizcloud.ir)</span>
          <span class="hljs-string">|
          v
</span>     <span class="hljs-string">Traefik</span> <span class="hljs-string">:443</span>    <span class="hljs-string">(TLS</span> <span class="hljs-string">termination</span> <span class="hljs-string">using</span> <span class="hljs-string">custom</span> <span class="hljs-string">cert)</span>
          <span class="hljs-string">|
          v
</span>    <span class="hljs-string">Jitsi</span> <span class="hljs-string">Web</span> <span class="hljs-string">:80</span>     <span class="hljs-string">(inside</span> <span class="hljs-string">Docker</span> <span class="hljs-string">network)</span>
          <span class="hljs-string">|</span>
          <span class="hljs-string">+--&gt;</span> <span class="hljs-string">Prosody</span> <span class="hljs-string">/</span> <span class="hljs-string">Jicofo</span> <span class="hljs-string">/</span> <span class="hljs-string">JVB</span> <span class="hljs-string">...</span>
</code></pre>
<hr />
<h2 id="heading-2-get-the-official-jitsi-docker-compose-files-release-zip">2) Get the Official Jitsi Docker Compose Files (Release ZIP)</h2>
<p>We keep all Docker-based apps under a single parent folder so backup is easy (zip the parent folder).</p>
<p>Create the folder structure:</p>
<pre><code class="lang-bash">mkdir -p docker/jitsi
<span class="hljs-built_in">cd</span> docker/jitsi
</code></pre>
<p>Download the latest official <code>docker-jitsi-meet</code> release ZIP:</p>
<pre><code class="lang-bash">wget $(curl -s https://api.github.com/repos/jitsi/docker-jitsi-meet/releases/latest | grep <span class="hljs-string">'zip'</span> | cut -d\" -f4)
</code></pre>
<p>Install <code>unzip</code>:</p>
<h3 id="heading-ubuntu-debian">Ubuntu / Debian</h3>
<pre><code class="lang-bash">sudo apt install unzip
</code></pre>
<h3 id="heading-redhat-centos-fedora">RedHat / CentOS / Fedora</h3>
<pre><code class="lang-bash">sudo dnf install unzip
</code></pre>
<p>Unzip the downloaded release (replace <code>&lt;version&gt;</code> with the actual filename you downloaded):</p>
<pre><code class="lang-bash">unzip ./stable-&lt;version&gt;
</code></pre>
<p>Rename the extracted folder to something clean:</p>
<pre><code class="lang-bash">mv jitsi-docker-jitsi-meet-&lt;hash-or-version&gt; jitsi-meet
<span class="hljs-built_in">cd</span> jitsi-meet
</code></pre>
<hr />
<h2 id="heading-3-copy-environment-and-compose-files">3) Copy Environment and Compose Files</h2>
<p>Create your working <code>.env</code> from the example:</p>
<pre><code class="lang-bash">cp env.example .env
</code></pre>
<p>Copy the compose file to the newer naming convention (<code>compose.yaml</code>) and back up the original:</p>
<pre><code class="lang-bash">cp docker-compose.yml compose.yaml
mv docker-compose.yml docker-compose.yml.bak
</code></pre>
<hr />
<h2 id="heading-4-edit-env-config-path-domain-ips-and-internal-authentication">4) Edit <code>.env</code> (Config Path, Domain, IPs, and Internal Authentication)</h2>
<p>Open <code>.env</code>:</p>
<pre><code class="lang-bash">nano .env
</code></pre>
<h3 id="heading-41-put-the-config-volume-inside-the-project-directory">4.1 Put the config volume inside the project directory</h3>
<p>This keeps all persistent configuration under the project folder (easy backups):</p>
<pre><code class="lang-yaml"><span class="hljs-string">CONFIG=./.jitsi-meet-cfg</span>
</code></pre>
<h3 id="heading-42-ports">4.2 Ports</h3>
<p>If the default ports (<code>8000</code>, <code>8443</code>) are free, you can keep them. If other apps use them, change them here and remember the values for your reverse proxy design.</p>
<h3 id="heading-43-timezone">4.3 Timezone</h3>
<p>Set the correct timezone for your server, for example:</p>
<pre><code class="lang-yaml"><span class="hljs-string">TZ=Europe/Berlin</span>
</code></pre>
<h3 id="heading-44-publicurl">4.4 PUBLIC_URL</h3>
<p>This is very important. It must match the <strong>external URL</strong> users will browse to:</p>
<pre><code class="lang-yaml"><span class="hljs-string">PUBLIC_URL=https://meet.shonizcloud.ir</span>
</code></pre>
<h3 id="heading-45-jvbadvertiseips">4.5 JVB_ADVERTISE_IPS</h3>
<p>For NAT or multi-path access scenarios, you can set:</p>
<pre><code class="lang-yaml"><span class="hljs-string">JVB_ADVERTISE_IPS=192.168.21.14,209.41.5.158</span>
</code></pre>
<p><strong>Very important note:</strong><br />The <strong>public IP</strong> you put here must be <strong>the exact public IP address that external users see and connect to</strong> (i.e., the Internet/WAN-facing IP).<br />If your server is behind NAT/firewall, this should <strong>not</strong> be your internal/private IP—use the <strong>public (NATed) IP</strong> that inbound traffic actually reaches. Optionally, you can include your private LAN IP first, then the public IP.</p>
<h3 id="heading-46-enable-internal-authentication-host-authentication">4.6 Enable Internal Authentication (Host Authentication)</h3>
<p>In the Authentication section, enable:</p>
<pre><code class="lang-yaml"><span class="hljs-string">ENABLE_AUTH=1</span>
<span class="hljs-string">ENABLE_GUESTS=1</span>
<span class="hljs-string">AUTH_TYPE=internal</span>
</code></pre>
<ul>
<li><p>With <code>ENABLE_GUESTS=1</code>, unauthenticated users can join once a host starts the meeting.</p>
</li>
<li><p>If you don’t want guests at all, do not enable <code>ENABLE_GUESTS</code>.</p>
</li>
</ul>
<h3 id="heading-47-generate-strong-internal-passwords">4.7 Generate Strong Internal Passwords</h3>
<p>Exit the editor and run:</p>
<pre><code class="lang-bash">./gen-passwords.sh
</code></pre>
<p>Re-open <code>.env</code> and verify the Security section got populated with strong random passwords.</p>
<h3 id="heading-48-enable-restart-policy">4.8 Enable Restart Policy</h3>
<p>Set:</p>
<pre><code class="lang-yaml"><span class="hljs-string">RESTART_POLICY=unless-stopped</span>
</code></pre>
<hr />
<h2 id="heading-5-pull-images-and-start-jitsi">5) Pull Images and Start Jitsi</h2>
<p>Pull all images:</p>
<pre><code class="lang-bash">docker compose pull
</code></pre>
<p>Start the stack and follow logs:</p>
<pre><code class="lang-bash">docker compose up -d &amp;&amp; docker compose logs -f
</code></pre>
<hr />
<h2 id="heading-6-create-prosody-users-for-authentication-recommended-use-default-meetjitsi">6) Create Prosody Users for Authentication (Recommended: Use Default <code>meet.jitsi</code>)</h2>
<p>If you try to create a Prosody user and see:</p>
<pre><code>The given hostname does not exist <span class="hljs-keyword">in</span> the config
</code></pre><p>it means the domain you used is not a configured <code>VirtualHost</code> in Prosody (or you are pointing <code>prosodyctl</code> at the wrong config).</p>
<p>To see the configured VirtualHosts:</p>
<pre><code class="lang-bash">docker compose <span class="hljs-built_in">exec</span> prosody sh -lc <span class="hljs-string">"grep -RIn 'VirtualHost' /config | head -n 50"</span>
</code></pre>
<p>In <code>docker-jitsi-meet</code>, VirtualHosts are commonly defined here:</p>
<ul>
<li><code>/config/conf.d/jitsi-meet.cfg.lua</code></li>
</ul>
<blockquote>
<p><strong>Recommendation:</strong> To avoid unnecessary complexity with XMPP domain changes, create users using the default internal XMPP domain <code>meet.jitsi</code>, even if your external web domain is <a target="_blank" href="http://meet.shonizcloud.ir"><code>meet.shonizcloud.ir</code></a>.</p>
</blockquote>
<p>Create a user like this:</p>
<pre><code class="lang-bash">docker compose <span class="hljs-built_in">exec</span> prosody prosodyctl --config /config/conf.d/jitsi-meet.cfg.lua \
  register mahdi meet.jitsi mahdi123
</code></pre>
<hr />
<h2 id="heading-7-put-jitsi-behind-traefik">7) Put Jitsi Behind Traefik</h2>
<p>In this design, Traefik handles HTTPS and forwards traffic internally to Jitsi over HTTP (port 80 inside the Docker network).</p>
<hr />
<h2 id="heading-8-full-traefik-configuration-for-custom-ssl-pemkey-using-file-provider">8) Full Traefik Configuration for Custom SSL (PEM/KEY) Using File Provider</h2>
<p>We disabled Let’s Encrypt for this site and used our own certificate.</p>
<h3 id="heading-81-traefik-docker-compose-full-config-used">8.1 Traefik Docker Compose (full config used)</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">"docker-mirror.kubarcloud.com/traefik"</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--api.insecure=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--providers.docker=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--providers.docker.exposedbydefault=false"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--providers.file.filename=/dynamic/tls.yml"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--providers.file.watch=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--entrypoints.web.address=:80"</span>
     <span class="hljs-comment"># - "--entrypoints.web.http.redirections.entryPoint.to=websecure"</span>
     <span class="hljs-comment"># - "--entrypoints.web.http.redirections.entrypoint.scheme=https"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--entrypoints.websecure.address=:443"</span>
    <span class="hljs-comment">#  - "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true"</span>
    <span class="hljs-comment">#  - "--certificatesresolvers.mytlschallenge.acme.email=mahdishadi99@gmail.com"</span>
    <span class="hljs-comment">#  - "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"80:80"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"443:443"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:8080"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">radar</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik_data:/letsencrypt</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/root/Dashboard-Internet/dynamic:/dynamic:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/root/Jitsi-meet/keys:/etc/traefik/certs:ro</span>
</code></pre>
<p>Notes:</p>
<ul>
<li><p><code>--providers.file.filename=/dynamic/tls.yml</code> tells Traefik to load TLS certs from this file.</p>
</li>
<li><p>We mount the cert directory so Traefik can read the PEM/KEY inside the container:</p>
<ul>
<li><code>/root/Jitsi-meet/keys</code> → <code>/etc/traefik/certs</code></li>
</ul>
</li>
</ul>
<h3 id="heading-82-tls-config-file-tlsyml">8.2 TLS config file (<code>tls.yml</code>)</h3>
<p>Path on the host:</p>
<p><code>/root/Dashboard-Internet/dynamic/tls.yml</code></p>
<p>Content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">tls:</span>
  <span class="hljs-attr">certificates:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">certFile:</span> <span class="hljs-string">/etc/traefik/certs/meet.shonizcloud.ir.pem</span>
      <span class="hljs-attr">keyFile:</span> <span class="hljs-string">/etc/traefik/certs/meet.shonizcloud.ir.key</span>
</code></pre>
<p>This means the following files must exist on the host:</p>
<ul>
<li><p><code>/root/Jitsi-meet/keys/</code><a target="_blank" href="http://meet.shonizcloud.ir"><code>meet.shonizcloud.ir</code></a><code>.pem</code></p>
</li>
<li><p><code>/root/Jitsi-meet/keys/</code><a target="_blank" href="http://meet.shonizcloud.ir"><code>meet.shonizcloud.ir</code></a><code>.key</code></p>
</li>
</ul>
<hr />
<h2 id="heading-9-jitsi-labels-for-traefik-https-redirect-security-headers">9) Jitsi Labels for Traefik (HTTPS + Redirect + Security Headers)</h2>
<p>Apply these labels to the <strong>Jitsi web</strong> service to route <a target="_blank" href="http://meet.shonizcloud.ir"><code>meet.shonizcloud.ir</code></a> through Traefik:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">labels:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.enable=true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet.rule=Host(`meet.shonizcloud.ir`)</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet.entrypoints=websecure</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet.tls=true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet.middlewares=meet-headers@docker</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.services.meet-svc.loadbalancer.server.port=80</span>

  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-headers.headers.STSSeconds=315360000</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-headers.headers.forceSTSHeader=true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-headers.headers.STSIncludeSubdomains=true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-headers.headers.STSPreload=true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-headers.headers.browserXSSFilter=true</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-headers.headers.contentTypeNosniff=true</span>

  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet-http.rule=Host(`meet.shonizcloud.ir`)</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet-http.entrypoints=web</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.meet-http.middlewares=meet-redirect@docker</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.meet-redirect.redirectscheme.scheme=https</span>
</code></pre>
<p>Key points:</p>
<ul>
<li><p><code>websecure</code> router serves HTTPS.</p>
</li>
<li><p>Separate <code>web</code> router redirects HTTP → HTTPS.</p>
</li>
<li><p><code>loadbalancer.server.port=80</code> tells Traefik to forward to the Jitsi web container on port 80.</p>
</li>
</ul>
<hr />
<h2 id="heading-10-verify-the-certificate-served-by-traefik">10) Verify the Certificate Served by Traefik</h2>
<p>To confirm Traefik is presenting your custom certificate:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> | openssl s_client -connect meet.shonizcloud.ir:443 -servername meet.shonizcloud.ir 2&gt;/dev/null \
  | openssl x509 -noout -subject -issuer -dates
</code></pre>
<hr />
<h2 id="heading-11-summary">11) Summary</h2>
<p>What we achieved:</p>
<ul>
<li><p>Downloaded and deployed <code>docker-jitsi-meet</code> from the latest official release ZIP</p>
</li>
<li><p>Organized the project for easy backup (<code>docker/jitsi/jitsi-meet</code>)</p>
</li>
<li><p>Configured <code>.env</code> (including internal auth + generated strong passwords)</p>
</li>
<li><p>Started the Jitsi stack using Docker Compose</p>
</li>
<li><p>Recommended creating Prosody users on the default XMPP domain <code>meet.jitsi</code></p>
</li>
<li><p>Configured Traefik to terminate TLS using a custom PEM/KEY certificate via File Provider</p>
</li>
<li><p>Added complete Traefik labels to route Jitsi over HTTPS and redirect HTTP to HTTPS</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[When Logs Weren’t Enough: Setting Up Prometheus Behind Traefik (TLS)]]></title><description><![CDATA[At some point, logs alone stop being enough. You start seeing short spikes in CPU, random latency jumps, or brief outages that don’t leave a clear trace in the logs. That’s where real monitoring pays off: numbers, charts, history, and alerts.
This gu...]]></description><link>https://blog.mahdishadi.me/when-logs-werent-enough-setting-up-prometheus-behind-traefik-tls</link><guid isPermaLink="true">https://blog.mahdishadi.me/when-logs-werent-enough-setting-up-prometheus-behind-traefik-tls</guid><category><![CDATA[#prometheus]]></category><category><![CDATA[Traefik]]></category><category><![CDATA[nginx]]></category><category><![CDATA[TLS]]></category><category><![CDATA[#nodeexporter]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Fri, 30 Jan 2026 12:31:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769775310507/0476a691-b096-4a61-ac91-020198f574d3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At some point, logs alone stop being enough. You start seeing short spikes in CPU, random latency jumps, or brief outages that don’t leave a clear trace in the logs. That’s where real monitoring pays off: numbers, charts, history, and alerts.</p>
<p>This guide walks through a complete, practical setup:</p>
<ul>
<li><p>Run <strong>Prometheus</strong> with <strong>Docker Compose</strong></p>
</li>
<li><p>Put Prometheus behind <strong>Traefik</strong> with <strong>TLS</strong></p>
</li>
<li><p>Fix a common UI issue in newer Prometheus releases</p>
</li>
<li><p>Install <strong>Node Exporter</strong> on the host using <strong>systemd</strong></p>
</li>
<li><p>Connect containerized Prometheus to host-installed Node Exporter</p>
</li>
<li><p>Put <strong>Node Exporter behind Traefik</strong> as well, so it’s reachable via <strong>HTTPS</strong> (secured with an IP whitelist)</p>
</li>
</ul>
<hr />
<h2 id="heading-what-is-prometheus">What is Prometheus?</h2>
<p><strong>Prometheus</strong> is a time-series monitoring system. Exporters and services usually expose metrics on an HTTP endpoint such as <code>/metrics</code>. Prometheus periodically <strong>scrapes</strong> these endpoints, stores the data, and lets you query it using <strong>PromQL</strong> (and later build dashboards/alerts with Grafana/Alertmanager).</p>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Docker + Docker Compose installed on your server</p>
</li>
<li><p>Traefik already running and capable of issuing TLS certificates (e.g., <code>certresolver=mytlschallenge</code>)</p>
</li>
<li><p>A Docker external network named <code>traefik</code></p>
</li>
<li><p>DNS records in place for:</p>
<ul>
<li><p><a target="_blank" href="http://prometheus.mahdishadi.me"><code>prometheus.mahdishadi.me</code></a></p>
</li>
<li><p><a target="_blank" href="http://nodeexporter.mahdishadi.me"><code>nodeexporter.mahdishadi.me</code></a></p>
</li>
</ul>
</li>
</ul>
<hr />
<h1 id="heading-part-1-run-prometheus-behind-traefik-with-tls">Part 1: Run Prometheus behind Traefik with TLS</h1>
<h2 id="heading-1-create-a-project-directory">1) Create a project directory</h2>
<pre><code class="lang-bash">mkdir -p /opt/monitoring/prometheus
<span class="hljs-built_in">cd</span> /opt/monitoring/prometheus
</code></pre>
<h2 id="heading-2-create-prometheusyml">2) Create <code>prometheus.yml</code></h2>
<p>Start simple: scrape Prometheus itself.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">15s</span>

<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">"prometheus"</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">"prometheus:9090"</span>]
</code></pre>
<h2 id="heading-3-create-docker-composeyml-prometheus-behind-traefik">3) Create <code>docker-compose.yml</code> (Prometheus behind Traefik)</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3.8"</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">prom/prometheus:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">prometheus</span>

    <span class="hljs-comment"># Useful for debugging. If you want Traefik-only access, you can remove this.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9090:9090"</span>

    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.enable=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.prometheus.rule=Host(`prometheus.mahdishadi.me`)</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.prometheus.entrypoints=web,websecure</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.prometheus.tls=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.prometheus.tls.certresolver=mytlschallenge</span>

      <span class="hljs-comment"># Optional security headers</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.SSLRedirect=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.STSSeconds=315360000</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.browserXSSFilter=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.contentTypeNosniff=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.forceSTSHeader=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.STSIncludeSubdomains=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.middlewares.prometheus.headers.STSPreload=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik.http.routers.prometheus.middlewares=prometheus@docker</span>

    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./prometheus.yml:/etc/prometheus/prometheus.yml:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">prometheus-data:/prometheus</span>

    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--config.file=/etc/prometheus/prometheus.yml"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--storage.tsdb.path=/prometheus"</span>
      <span class="hljs-comment"># Helps if the new UI loads blank/spins on /query in some setups</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--enable-feature=old-ui"</span>

    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">traefik</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">prometheus-data:</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-4-start-prometheus">4) Start Prometheus</h2>
<pre><code class="lang-bash">docker compose up -d
</code></pre>
<p>Access:</p>
<ul>
<li><a target="_blank" href="https://prometheus.mahdishadi.me"><code>https://prometheus.mahdishadi.me</code></a></li>
</ul>
<hr />
<h1 id="heading-part-2-if-the-ui-is-blank-verify-prometheus-is-healthy">Part 2: If the UI is blank, verify Prometheus is healthy</h1>
<p>If the UI is stuck/blank, check the server directly via API endpoints:</p>
<pre><code class="lang-bash">curl -sS http://127.0.0.1:9090/-/ready
curl -sS http://127.0.0.1:9090/api/v1/status/buildinfo | head
curl -sS <span class="hljs-string">'http://127.0.0.1:9090/api/v1/query?query=up'</span>
</code></pre>
<ul>
<li><p>If you get “Ready” and the <code>up</code> query returns a result, Prometheus is healthy and it’s likely a frontend/UI issue.</p>
</li>
<li><p>Enabling <code>--enable-feature=old-ui</code> (already included above) typically fixes this scenario.</p>
</li>
</ul>
<hr />
<h1 id="heading-part-3-install-node-exporter-on-the-host-systemd">Part 3: Install Node Exporter on the host (systemd)</h1>
<p>Node Exporter provides host-level metrics: CPU, memory, disk, network, load, etc.</p>
<h2 id="heading-1-create-a-dedicated-user">1) Create a dedicated user</h2>
<pre><code class="lang-bash">sudo useradd --no-create-home --shell /usr/sbin/nologin node_exporter
</code></pre>
<h2 id="heading-2-check-system-architecture">2) Check system architecture</h2>
<pre><code class="lang-bash">uname -m
</code></pre>
<ul>
<li><p><code>x86_64</code> → <code>linux-amd64</code></p>
</li>
<li><p><code>aarch64</code> → <code>linux-arm64</code></p>
</li>
</ul>
<h2 id="heading-3-download-and-install-the-binary-example-amd64">3) Download and install the binary (example: amd64)</h2>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> /tmp
VER=<span class="hljs-string">"1.8.1"</span>

wget https://github.com/prometheus/node_exporter/releases/download/v<span class="hljs-variable">${VER}</span>/node_exporter-<span class="hljs-variable">${VER}</span>.linux-amd64.tar.gz
tar xvf node_exporter-<span class="hljs-variable">${VER}</span>.linux-amd64.tar.gz

sudo mv node_exporter-<span class="hljs-variable">${VER}</span>.linux-amd64/node_exporter /usr/<span class="hljs-built_in">local</span>/bin/
sudo chown node_exporter:node_exporter /usr/<span class="hljs-built_in">local</span>/bin/node_exporter
sudo chmod +x /usr/<span class="hljs-built_in">local</span>/bin/node_exporter
</code></pre>
<p><em>(If you’re on ARM64, download</em> <code>node_exporter-${VER}.linux-arm64.tar.gz</code> instead.)</p>
<h2 id="heading-4-create-a-systemd-service">4) Create a systemd service</h2>
<pre><code class="lang-bash">sudo tee /etc/systemd/system/node_exporter.service &gt; /dev/null &lt;&lt;<span class="hljs-string">'EOF'</span>
[Unit]
Description=Prometheus Node Exporter
Wants=network-online.target
After=network-online.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/<span class="hljs-built_in">local</span>/bin/node_exporter --web.listen-address=0.0.0.0:9100
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
</code></pre>
<h2 id="heading-5-enable-and-start-the-service">5) Enable and start the service</h2>
<pre><code class="lang-bash">sudo systemctl daemon-reload
sudo systemctl <span class="hljs-built_in">enable</span> --now node_exporter
sudo systemctl status node_exporter --no-pager
</code></pre>
<h2 id="heading-6-test-it-locally">6) Test it locally</h2>
<pre><code class="lang-bash">curl -sS http://127.0.0.1:9100/metrics | head
</code></pre>
<hr />
<h1 id="heading-part-4-connect-containerized-prometheus-to-host-installed-node-exporter">Part 4: Connect containerized Prometheus to host-installed Node Exporter</h1>
<p>On Linux, <code>host.docker.internal</code> is not always available by default inside containers. The clean solution is to map it using Docker’s <code>host-gateway</code>.</p>
<h2 id="heading-1-add-extrahosts-to-prometheus-in-docker-composeyml">1) Add <code>extra_hosts</code> to Prometheus in <code>docker-compose.yml</code></h2>
<pre><code class="lang-yaml"><span class="hljs-attr">extra_hosts:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"host.docker.internal:host-gateway"</span>
</code></pre>
<p>Example (relevant section):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-string">...</span>
    <span class="hljs-attr">extra_hosts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"host.docker.internal:host-gateway"</span>
</code></pre>
<p>Then recreate:</p>
<pre><code class="lang-bash">docker compose up -d --force-recreate
</code></pre>
<h2 id="heading-2-add-the-node-exporter-job-to-prometheusyml">2) Add the Node Exporter job to <code>prometheus.yml</code></h2>
<pre><code class="lang-yaml">  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">"Mahdi Host"</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">"host.docker.internal:9100"</span>]
</code></pre>
<p>Final <code>prometheus.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">15s</span>

<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">"prometheus"</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">"prometheus:9090"</span>]

  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">"Mahdi Host"</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">"host.docker.internal:9100"</span>]
</code></pre>
<p>Recreate Prometheus:</p>
<pre><code class="lang-bash">docker compose up -d --force-recreate
</code></pre>
<p>In Prometheus UI:</p>
<ul>
<li><p><strong>Status → Targets</strong></p>
</li>
<li><p>The <code>Mahdi Host</code> job should be <strong>UP</strong></p>
</li>
</ul>
<hr />
<h1 id="heading-part-5-put-node-exporter-behind-traefik-with-https">Part 5: Put Node Exporter behind Traefik with HTTPS</h1>
<p>Node Exporter does not provide TLS by itself. To expose it via HTTPS, put Traefik in front of it and let Traefik terminate TLS.</p>
<h2 id="heading-1-ensure-traefik-can-reach-the-host">1) Ensure Traefik can reach the host</h2>
<p>In Traefik’s own compose file (the one that runs Traefik), add:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">extra_hosts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"host.docker.internal:host-gateway"</span>
</code></pre>
<h2 id="heading-2-enable-traefik-file-provider">2) Enable Traefik File Provider</h2>
<p>This is the clean approach for services that are not Docker containers.</p>
<p>In Traefik’s compose:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/opt/traefik/dynamic:/etc/traefik/dynamic</span>
    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--providers.file.directory=/etc/traefik/dynamic"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"--providers.file.watch=true"</span>
</code></pre>
<p>Create the directory:</p>
<pre><code class="lang-bash">sudo mkdir -p /opt/traefik/dynamic
</code></pre>
<h2 id="heading-3-create-a-dynamic-config-for-node-exporter-tls-ip-whitelist">3) Create a dynamic config for Node Exporter (TLS + IP whitelist)</h2>
<p>Create:</p>
<p><code>/opt/traefik/dynamic/nodeexporter.yml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">http:</span>
  <span class="hljs-attr">routers:</span>
    <span class="hljs-attr">nodeexporter:</span>
      <span class="hljs-attr">rule:</span> <span class="hljs-string">Host(`nodeexporter.mahdishadi.me`)</span>
      <span class="hljs-attr">entryPoints:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">websecure</span>
      <span class="hljs-attr">tls:</span>
        <span class="hljs-attr">certResolver:</span> <span class="hljs-string">mytlschallenge</span>
      <span class="hljs-attr">service:</span> <span class="hljs-string">nodeexporter</span>
      <span class="hljs-attr">middlewares:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">nodeexp-ipwhitelist</span>

  <span class="hljs-attr">services:</span>
    <span class="hljs-attr">nodeexporter:</span>
      <span class="hljs-attr">loadBalancer:</span>
        <span class="hljs-attr">servers:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">url:</span> <span class="hljs-string">"http://host.docker.internal:9100"</span>

  <span class="hljs-attr">middlewares:</span>
    <span class="hljs-attr">nodeexp-ipwhitelist:</span>
      <span class="hljs-attr">ipWhiteList:</span>
        <span class="hljs-attr">sourceRange:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"YOUR_PUBLIC_IP/32"</span>
</code></pre>
<p>Get your public IP:</p>
<pre><code class="lang-bash">curl -s https://ifconfig.me
</code></pre>
<p>Replace <code>YOUR_PUBLIC_IP/32</code> with the output, e.g. <code>1.2.3.4/32</code>.</p>
<p>Recreate Traefik:</p>
<pre><code class="lang-bash">docker compose up -d --force-recreate
</code></pre>
<p>Access (Node Exporter’s main endpoint):</p>
<ul>
<li><a target="_blank" href="https://nodeexporter.mahdishadi.me/metrics"><code>https://nodeexporter.mahdishadi.me/metrics</code></a></li>
</ul>
<blockquote>
<p>Node Exporter serves metrics on <code>/metrics</code>. Hitting <code>/</code> may not show useful output.</p>
</blockquote>
<hr />
<h1 id="heading-part-6-dealing-with-403-forbidden-on-node-exporter-https">Part 6: Dealing with <code>403 Forbidden</code> on Node Exporter HTTPS</h1>
<p>A 403 here is usually caused by the IP whitelist:</p>
<ul>
<li><p>Your current IP is not included in <code>sourceRange</code></p>
</li>
<li><p>You are on VPN (your public IP changed)</p>
</li>
</ul>
<p>Update <code>sourceRange</code> with your actual public IP and reload/recreate Traefik.</p>
<hr />
<h2 id="heading-quick-verification-commands">Quick verification commands</h2>
<h3 id="heading-node-exporter-directly-on-the-host">Node Exporter directly on the host</h3>
<pre><code class="lang-bash">curl -sS http://127.0.0.1:9100/metrics | head
</code></pre>
<h3 id="heading-node-exporter-via-traefik-https">Node Exporter via Traefik HTTPS</h3>
<pre><code class="lang-bash">curl -sS https://nodeexporter.mahdishadi.me/metrics | head
</code></pre>
<h3 id="heading-prometheus-container-scraping-the-host">Prometheus container scraping the host</h3>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it prometheus sh -c <span class="hljs-string">'wget -qO- http://host.docker.internal:9100/metrics | head'</span>
</code></pre>
<hr />
<h2 id="heading-security-note-important">Security note (important)</h2>
<p>Node Exporter has no authentication. If exposed to the internet, always protect it:</p>
<ul>
<li><p>IP whitelist (as shown), and/or</p>
</li>
<li><p>Basic auth, and/or</p>
</li>
<li><p>keep it internal-only and let Prometheus scrape it privately</p>
</li>
</ul>
<p>Prometheus scraping does not require HTTPS; HTTPS is mainly for safe human/browser access.</p>
]]></content:encoded></item><item><title><![CDATA[How I Put n8n Behind Traefik with Automatic HTTPS: A Real-World Traefik + n8n Setup]]></title><description><![CDATA[This all started with a pretty simple goal:

“Run n8n on my server and expose it cleanly athttps://n-test.mahdishadi.me.”

I could have just exposed n8n’s port directly on the server and called it a day.But I wanted something more:

Clean domain-base...]]></description><link>https://blog.mahdishadi.me/how-i-put-n8n-behind-traefik-with-automatic-https-a-real-world-traefik-n8n-setup</link><guid isPermaLink="true">https://blog.mahdishadi.me/how-i-put-n8n-behind-traefik-with-automatic-https-a-real-world-traefik-n8n-setup</guid><category><![CDATA[Traefik]]></category><category><![CDATA[n8n]]></category><category><![CDATA[containers]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Let's Encrypt]]></category><category><![CDATA[https]]></category><category><![CDATA[cloud native]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Tue, 02 Dec 2025 06:10:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764655506033/104fa55d-54f5-479c-baca-a9d8f1bc8cdd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This all started with a pretty simple goal:</p>
<blockquote>
<p>“Run <strong>n8n</strong> on my server and expose it cleanly at<br /><a target="_blank" href="https://n-test.mahdishadi.me.xn--ivg"><code>https://n-test.mahdishadi.me</code>.”</a></p>
</blockquote>
<p>I could have just exposed n8n’s port directly on the server and called it a day.<br />But I wanted something more:</p>
<ul>
<li><p>Clean domain-based routing</p>
</li>
<li><p>Proper HTTPS with Let’s Encrypt</p>
</li>
<li><p>A setup that scales nicely when I add more services</p>
</li>
</ul>
<p>So I went for a <strong>cloud-native</strong> approach with <strong>Traefik</strong> in front and n8n behind it in Docker.</p>
<p>Of course, it didn’t “just work.” There were 404s, 308s, ACME errors, missing volumes, and plenty of <code>docker exec</code> + <code>curl</code>.<br />Here’s the full story of what I did, why I did it, and how I debugged the problems along the way.</p>
<h2 id="heading-why-traefik-and-why-cloud-native-matters-here">Why Traefik? And Why Cloud-Native Matters Here</h2>
<h3 id="heading-what-is-traefik">What is Traefik?</h3>
<p>Traefik is an <strong>edge router / reverse proxy</strong> built for the cloud-native world.</p>
<p>Instead of manually editing a huge config file and defining all your backends, Traefik:</p>
<ul>
<li><p>Discovers services automatically (via Docker, Kubernetes, Consul, etc.)</p>
</li>
<li><p>Configures routes based on <strong>labels</strong> or <strong>Ingress</strong> definitions</p>
</li>
<li><p>Handles <strong>Let’s Encrypt</strong> and automatic certificate renewal</p>
</li>
<li><p>Offers middlewares (redirects, auth, rate limiting, etc.)</p>
</li>
<li><p>Ships with a web dashboard and metrics integration</p>
</li>
</ul>
<h3 id="heading-why-use-it-instead-of-just-exposing-n8n-directly">Why use it instead of just exposing n8n directly?</h3>
<p>When you have more than one service, the “just expose a random port” approach gets ugly fast:</p>
<ul>
<li><p>You end up with URLs like <code>server-ip:5678</code>, <code>server-ip:9000</code>, etc.</p>
</li>
<li><p>Managing HTTPS per service becomes painful.</p>
</li>
<li><p>Security and organization become a mess.</p>
</li>
</ul>
<p>With Traefik:</p>
<ul>
<li><p>You expose <strong>only</strong> Traefik to the internet (ports 80/443).</p>
</li>
<li><p>All internal services (like n8n) live on a private Docker network.</p>
</li>
<li><p>You route based on hostname (e.g. <a target="_blank" href="http://n-test.mahdishadi.me"><code>n-test.mahdishadi.me</code></a> → n8n).</p>
</li>
<li><p>SSL is handled centrally, automatically.</p>
</li>
</ul>
<h3 id="heading-what-does-cloud-native-buy-us-here">What does “cloud-native” buy us here?</h3>
<ul>
<li><p><strong>Dynamic service discovery</strong><br />  New containers with the correct labels are picked up automatically—no manual configuration reloads are required.</p>
</li>
<li><p><strong>Configuration via labels</strong><br />  Routing logic lives alongside the service definition (in <code>docker-compose.yml</code>), not in a giant central config file.</p>
</li>
<li><p><strong>Automatic TLS</strong><br />  Traefik talks to Let’s Encrypt via ACME, requests and renews certs, and stores them in a file you mount.</p>
</li>
<li><p><strong>Better security &amp; separation</strong><br />  Only Traefik is exposed. n8n is reachable only over an internal Docker network.</p>
</li>
</ul>
<h2 id="heading-the-final-architecture">The Final Architecture</h2>
<p>Here’s the architecture I aimed for:</p>
<ul>
<li><p><strong>Traefik</strong>:</p>
<ul>
<li><p>Listens on ports <code>80</code> and <code>443</code> for HTTP/HTTPS</p>
</li>
<li><p>Has a dashboard on port <code>8080</code></p>
</li>
<li><p>Talks to Docker and automatically discovers services</p>
</li>
<li><p>Gets Let’s Encrypt certs and stores them in <code>acme.json</code></p>
</li>
</ul>
</li>
<li><p><strong>n8n</strong> :</p>
<ul>
<li><p>Runs in Docker on a private network <code>frontend</code></p>
</li>
<li><p>Does <em>not</em> expose its port directly to the host</p>
</li>
<li><p>Is accessible only via Traefik at <a target="_blank" href="https://n-test.mahdishadi.me"><code>https://n-test.mahdishadi.me</code></a></p>
</li>
</ul>
</li>
<li><p><strong>Docker network</strong>:</p>
<ul>
<li>A shared network named <code>frontend</code> that both Traefik and n8n join</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-traefik-setup-entrypoints-docker-provider-and-lets-encrypt">Traefik Setup: EntryPoints, Docker Provider, and Let’s Encrypt</h2>
<h3 id="heading-docker-compose-for-traefik">docker-compose for Traefik</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">traefik:v3.6</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">traefik-demo</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"80:80"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"443:443"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:8080"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/traefik.yaml:/etc/traefik/traefik.yaml:ro</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./letsencrypt:/letsencrypt</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">frontend</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">frontend:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>Key ideas:</p>
<ul>
<li><p>The Docker socket lets Traefik read labels on your containers.</p>
</li>
<li><p>The config file is mounted as <code>traefik.yaml</code>.</p>
</li>
<li><p>The <code>./letsencrypt</code> directory is mounted into the container as <code>/letsencrypt</code> for cert storage.</p>
</li>
<li><p>Traefik is on the <code>frontend</code> network so it can reach n8n.</p>
</li>
</ul>
<h3 id="heading-traefikyaml-configuration"><code>traefik.yaml</code> configuration</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">checkNewVersion:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">sendAnonymousUsage:</span> <span class="hljs-literal">false</span>

<span class="hljs-attr">log:</span>
  <span class="hljs-attr">level:</span> <span class="hljs-string">DEBUG</span>

<span class="hljs-attr">api:</span>
  <span class="hljs-attr">dashboard:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">insecure:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">entryPoints:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">address:</span> <span class="hljs-string">":80"</span>
    <span class="hljs-attr">http:</span>
      <span class="hljs-attr">redirections:</span>             <span class="hljs-comment"># redirect HTTP -&gt; HTTPS</span>
        <span class="hljs-attr">entryPoint:</span>
          <span class="hljs-attr">to:</span> <span class="hljs-string">websecure</span>
          <span class="hljs-attr">scheme:</span> <span class="hljs-string">https</span>
  <span class="hljs-attr">websecure:</span>
    <span class="hljs-attr">address:</span> <span class="hljs-string">":443"</span>

<span class="hljs-attr">providers:</span>
  <span class="hljs-attr">docker:</span>
    <span class="hljs-attr">endpoint:</span> <span class="hljs-string">"unix://var/run/docker.sock"</span>
    <span class="hljs-attr">exposedByDefault:</span> <span class="hljs-literal">false</span>     
    <span class="hljs-attr">network:</span> <span class="hljs-string">frontend</span>          

<span class="hljs-attr">certificatesResolvers:</span>
  <span class="hljs-attr">letsencrypt:</span>
    <span class="hljs-attr">acme:</span>
      <span class="hljs-attr">email:</span> <span class="hljs-string">"mahdishadi99@gmail.com"</span>
      <span class="hljs-attr">storage:</span> <span class="hljs-string">"/letsencrypt/acme.json"</span>
      <span class="hljs-attr">httpChallenge:</span>
        <span class="hljs-attr">entryPoint:</span> <span class="hljs-string">"web"</span>
</code></pre>
<p>Why this config?</p>
<ul>
<li><p><code>exposedByDefault: false</code><br />  Traefik does <strong>not</strong> automatically publish every container. Only containers with <code>traefik.enable=true</code> become reachable. This is safer and more explicit.</p>
</li>
<li><p><code>network: frontend</code><br />  Traefik will use the <code>frontend</code> Docker network when talking to containers. n8n is on the same network, so they can talk.</p>
</li>
<li><p><code>entryPoints.web</code> → <code>websecure</code> redirect<br />  Everything on port 80 gets redirected to HTTPS on port 443. All public traffic ends up encrypted.</p>
</li>
<li><p><code>certificatesResolvers.letsencrypt</code> with <code>httpChallenge</code><br />  Traefik uses the HTTP-01 challenge on the entrypoint <code>web</code> (port 80) to obtain certificates and store them at <code>/letsencrypt/acme.json</code>.</p>
</li>
</ul>
<p>On the host side, I created the ACME storage file:</p>
<pre><code class="lang-yaml"><span class="hljs-string">mkdir</span> <span class="hljs-string">-p</span> <span class="hljs-string">letsencrypt</span>
<span class="hljs-string">touch</span> <span class="hljs-string">letsencrypt/acme.json</span>
<span class="hljs-string">chmod</span> <span class="hljs-number">600</span> <span class="hljs-string">letsencrypt/acme.json</span>
</code></pre>
<hr />
<h2 id="heading-n8n-setup-labels-and-environment">n8n Setup: Labels and Environment</h2>
<p>Here’s the n8n stack I ended up with:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3.8"</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">frontend:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>  

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">n8n:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">n8nio/n8n:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">n8n</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.enable=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N.rule=Host(`n-test.mahdishadi.me`)"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N.entrypoints=websecure"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N.tls.certresolver=letsencrypt"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N-http.rule=Host(`n-test.mahdishadi.me`)"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N-http.entrypoints=web"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N-http.middlewares=N8N-https-redirect"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.N8N-https-redirect.redirectscheme.scheme=https"</span>

    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_HOST=n-test.mahdishadi.me</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_PORT=5678</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_PROTOCOL=https</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">NODE_ENV=production</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBHOOK_URL=https://n-test.mahdishadi.me</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">GENERIC_TIMEZONE=Asia/Tehran</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_ACTIVE=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_USER=admin</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_BASIC_AUTH_PASSWORD=********</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_USER_MANAGEMENT_DISABLED=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_AUTH_DISABLE=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">N8N_DIAGNOSTICS_ENABLED=false</span>

    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">n8ntest_data:/home/node/.n8n</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./local-files:/files</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">frontend</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">n8ntest_data:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
</code></pre>
<h3 id="heading-why-these-traefik-labels">Why these Traefik labels?</h3>
<ul>
<li><p><code>traefik.enable=true</code><br />  Required because <code>exposedByDefault: false</code>. Without this, Traefik ignores the container completely.</p>
</li>
<li><p>Main HTTPS router:</p>
<pre><code class="lang-yaml">  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N.rule=Host(`n-test.mahdishadi.me`)"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N.entrypoints=websecure"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N.tls.certresolver=letsencrypt"</span>
</code></pre>
<ul>
<li><p>Matches only requests with <code>Host:</code> <a target="_blank" href="http://n-test.mahdishadi.me"><code>n-test.mahdishadi.me</code></a>.</p>
</li>
<li><p>Uses the <code>websecure</code> entrypoint (port 443).</p>
</li>
<li><p>Uses the <code>letsencrypt</code> resolver defined in <code>traefik.yaml</code> to get the certificate.</p>
</li>
</ul>
</li>
<li><p>HTTP router + redirect middleware:</p>
<pre><code class="lang-yaml">  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N-http.rule=Host(`n-test.mahdishadi.me`)"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N-http.entrypoints=web"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.N8N-http.middlewares=N8N-https-redirect"</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.N8N-https-redirect.redirectscheme.scheme=https"</span>
</code></pre>
<p>  This router listens on <code>web</code> (port 80) and applies the <code>N8N-https-redirect</code> middleware, which simply changes the scheme to <code>https</code>. That means:</p>
<ul>
<li><a target="_blank" href="http://n-test.mahdishadi.me"><code>http://n-test.mahdishadi.me</code></a> → 301/308 → <a target="_blank" href="https://n-test.mahdishadi.me"><code>https://n-test.mahdishadi.me</code></a>.</li>
</ul>
</li>
</ul>
<h3 id="heading-why-these-n8n-environment-variables">Why these n8n environment variables?</h3>
<ul>
<li><p><code>N8N_HOST</code>, <code>N8N_PORT</code>, <code>N8N_PROTOCOL</code><br />  n8n must know how it is accessed from the outside so it can generate correct URLs.</p>
</li>
<li><p><code>WEBHOOK_URL</code><br />  Used for webhook URLs n8n exposes; it must match the external HTTPS URL.</p>
</li>
<li><p><code>GENERIC_TIMEZONE=Asia/Tehran</code><br />  So scheduled workflows match your local time.</p>
</li>
<li><p><code>N8N_BASIC_AUTH_*</code><br />  Adds an extra Basic Auth layer in front of the n8n UI. (For a public internet-facing instance, this is very helpful.)</p>
</li>
<li><p><code>N8N_USER_MANAGEMENT_DISABLED</code>, <code>N8N_AUTH_DISABLE</code><br />  Disables n8n’s internal user management/auth, relying instead on the Basic Auth above for this scenario.</p>
</li>
<li><p><code>N8N_DIAGNOSTICS_ENABLED=false</code><br />  Disables telemetry/diagnostics.</p>
</li>
</ul>
<hr />
<h2 id="heading-the-problems-i-hit-and-how-they-were-fixed">The Problems I Hit (and How They Were Fixed)</h2>
<h3 id="heading-problem-1-404-from-traefik-no-router-matched">Problem 1: 404 from Traefik – No Router Matched</h3>
<p>One of the first tests I ran was:</p>
<pre><code class="lang-yaml"><span class="hljs-string">curl</span> <span class="hljs-string">-v</span> <span class="hljs-string">http://91.107.179.182/</span> <span class="hljs-string">-H</span> <span class="hljs-string">'Host: n-test.mahdishadi.me'</span>
</code></pre>
<p>And I got:</p>
<pre><code class="lang-yaml"><span class="hljs-string">HTTP/1.1</span> <span class="hljs-number">404</span> <span class="hljs-string">Not</span> <span class="hljs-string">Found</span>
<span class="hljs-number">404</span> <span class="hljs-string">page</span> <span class="hljs-string">not</span> <span class="hljs-string">found</span>
</code></pre>
<p>This means:</p>
<ul>
<li><p>The request <em>did</em> reach Traefik (good).</p>
</li>
<li><p>But Traefik didn’t find any router that matched <code>Host("</code><a target="_blank" href="http://n-test.mahdishadi.me"><code>n-test.mahdishadi.me</code></a><code>")</code> on that entrypoint (bad).</p>
</li>
</ul>
<p>Possible causes I checked:</p>
<ul>
<li><p>Wrong or missing labels</p>
</li>
<li><p>Docker provider is not working correctly</p>
</li>
<li><p>Service isn’t on the same network as Traefik</p>
</li>
</ul>
<p>By fixing the Docker provider config (<code>exposedByDefault</code>, <code>network: frontend</code>) and ensuring both Traefik and n8n were attached to the <code>frontend</code> network, Traefik started picking up the routers correctly.</p>
<h3 id="heading-problem-2-http-https-worked-308-but-https-still-failed">Problem 2: HTTP → HTTPS Worked (308), but HTTPS Still Failed</h3>
<p>After adding the HTTP router and redirect middleware, I tested:</p>
<pre><code class="lang-yaml"><span class="hljs-string">curl</span> <span class="hljs-string">-I</span> <span class="hljs-string">http://n-test.mahdishadi.me</span>
</code></pre>
<p>Result:</p>
<pre><code class="lang-yaml"><span class="hljs-string">HTTP/1.1</span> <span class="hljs-number">308</span> <span class="hljs-string">Permanent</span> <span class="hljs-string">Redirect</span>
<span class="hljs-attr">Location:</span> <span class="hljs-string">https://n-test.mahdishadi.me/</span>
</code></pre>
<p>So the HTTP side looked great:</p>
<ul>
<li><p><code>N8N-http</code> router was active.</p>
</li>
<li><p>Middleware <code>N8N-https-redirect</code> did its job.</p>
</li>
</ul>
<p>But the next step was making sure <strong>HTTPS itself</strong> worked with a valid certificate. That’s where Let’s Encrypt and ACME came in.</p>
<hr />
<h3 id="heading-problem-3-lets-encrypt-acme-errors-http-challenge-not-enabled-resolver-skipped">Problem 3: Let’s Encrypt / ACME Errors – “HTTP challenge not enabled” / Resolver Skipped</h3>
<p>I configured ACME in <code>traefik.yaml</code> and created <code>letsencrypt/acme.json</code> with permissions <code>600</code>.<br />Everything <em>looked</em> correct… but Traefik logs / dashboard complained:</p>
<ul>
<li><p>The HTTP challenge wasn’t enabled, or</p>
</li>
<li><p>The <code>letsencrypt</code> Resolver was being skipped.</p>
</li>
</ul>
<p>So I jumped into the container to see if the ACME storage path really existed:</p>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">exec</span> <span class="hljs-string">-it</span> <span class="hljs-string">traefik-demo</span> <span class="hljs-string">sh</span>
<span class="hljs-string">/</span> <span class="hljs-comment"># ls /letsencrypt</span>
<span class="hljs-attr">ls:</span> <span class="hljs-string">can't</span> <span class="hljs-string">access</span> <span class="hljs-string">'/letsencrypt'</span><span class="hljs-string">:</span> <span class="hljs-literal">No</span> <span class="hljs-string">such</span> <span class="hljs-string">file</span> <span class="hljs-string">or</span> <span class="hljs-string">directory</span>
</code></pre>
<p>Boom. There it was.</p>
<h3 id="heading-root-cause-the-volume-wasnt-mounted-old-container-new-compose">Root cause: the volume wasn’t mounted (old container, new compose)</h3>
<p>Yes, my <code>docker-compose.yml</code> had:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">volumes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">./letsencrypt:/letsencrypt</span>
</code></pre>
<p>But I had <strong>not</strong> recreated the container after adding that. I had only restarted it.<br />Docker will not magically pick up new volumes on an existing container; you must recreate it.</p>
<p>The fix:</p>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">compose</span> <span class="hljs-string">down</span>
<span class="hljs-string">docker</span> <span class="hljs-string">compose</span> <span class="hljs-string">up</span> <span class="hljs-string">-d</span>
</code></pre>
<p>Then:</p>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">exec</span> <span class="hljs-string">-it</span> <span class="hljs-string">traefik-demo</span> <span class="hljs-string">sh</span>
<span class="hljs-string">/</span> <span class="hljs-comment"># ls /letsencrypt</span>
<span class="hljs-string">acme.json</span>
<span class="hljs-string">/</span> <span class="hljs-comment"># ls -l /letsencrypt/acme.json</span>
<span class="hljs-string">-rw-------</span> <span class="hljs-number">1</span> <span class="hljs-string">root</span> <span class="hljs-string">root</span> <span class="hljs-number">0</span> <span class="hljs-string">Nov</span> <span class="hljs-number">28</span> <span class="hljs-number">05</span><span class="hljs-string">:51</span> <span class="hljs-string">acme.json</span>
</code></pre>
<p>Now Traefik could actually access <code>/letsencrypt/acme.json</code>, and the ACME resolver started working properly. Once the certificate was obtained, <code>acme.json</code> grew from 0 bytes to a real JSON file with cert data.</p>
<p>At that point, hitting:</p>
<pre><code class="lang-yaml"><span class="hljs-string">curl</span> <span class="hljs-string">-vk</span> <span class="hljs-string">https://n-test.mahdishadi.me/</span>
</code></pre>
<p>returned the n8n HTML with a valid Let’s Encrypt certificate, and browsers showed a secure connection.</p>
<hr />
<h2 id="heading-debugging-traefik-n8n-a-practical-checklist">Debugging Traefik + n8n: A Practical Checklist</h2>
<p>This journey turned into a pretty solid debugging checklist:</p>
<h3 id="heading-1-always-start-with-curl-host-header">1. Always start with <code>curl</code> + <code>Host</code> header</h3>
<pre><code class="lang-yaml"><span class="hljs-string">curl</span> <span class="hljs-string">-v</span> <span class="hljs-string">http://SERVER_IP/</span> <span class="hljs-string">-H</span> <span class="hljs-string">'Host: n-test.mahdishadi.me'</span>
<span class="hljs-string">curl</span> <span class="hljs-string">-vk</span> <span class="hljs-string">https://n-test.mahdishadi.me/</span> <span class="hljs-string">-H</span> <span class="hljs-string">'Host: n-test.mahdishadi.me'</span>
</code></pre>
<p>What errors mean:</p>
<ul>
<li><p><strong>404</strong> → No matching Traefik router (rule/labels/entrypoint issue).</p>
</li>
<li><p><strong>308</strong> → HTTP redirect is working.</p>
</li>
<li><p><strong>502</strong> → Router exists, but backend (n8n) is unreachable (network/port/container down).</p>
</li>
</ul>
<h3 id="heading-2-use-the-traefik-dashboard">2. Use the Traefik dashboard</h3>
<p>Visit:<br /><a target="_blank" href="http://SERVER_IP:8080/dashboard/"><code>http://SERVER_IP:8080/dashboard/</code></a></p>
<p>Check:</p>
<ul>
<li><p><strong>Routers</strong>:</p>
<ul>
<li><p>Do you see <code>N8N</code> and <code>N8N-http</code>?</p>
</li>
<li><p>Do they have <code>Rule = Host("</code><a target="_blank" href="http://n-test.mahdishadi.me"><code>n-test.mahdishadi.me</code></a><code>")</code>?</p>
</li>
<li><p>Are the entrypoints <code>web</code> / <code>websecure</code> as expected?</p>
</li>
</ul>
</li>
<li><p><strong>Services</strong>:</p>
<ul>
<li><p>Is there a service backing <code>N8N</code>?</p>
</li>
<li><p>Is it green (healthy) or red (error)?</p>
</li>
<li><p>What internal IP/port is Traefik using?</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-3-verify-docker-networking">3. Verify Docker networking</h3>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">network</span> <span class="hljs-string">inspect</span> <span class="hljs-string">frontend</span>
<span class="hljs-string">docker</span> <span class="hljs-string">inspect</span> <span class="hljs-string">traefik-demo</span> <span class="hljs-string">|</span> <span class="hljs-string">grep</span> <span class="hljs-string">-A3</span> <span class="hljs-string">'"frontend"'</span>
<span class="hljs-string">docker</span> <span class="hljs-string">inspect</span> <span class="hljs-string">n8n</span> <span class="hljs-string">|</span> <span class="hljs-string">grep</span> <span class="hljs-string">-A3</span> <span class="hljs-string">'"frontend"'</span>
</code></pre>
<p>Both containers must be attached to the same network (<code>frontend</code>) or Traefik can’t reach n8n.</p>
<h3 id="heading-4-double-check-acme-lets-encrypt">4. Double-check ACME / Let’s Encrypt</h3>
<p>On the host:</p>
<pre><code class="lang-yaml"><span class="hljs-string">ls</span> <span class="hljs-string">-l</span> <span class="hljs-string">letsencrypt</span>
<span class="hljs-comment"># acme.json should be -rw------- (chmod 600)</span>
</code></pre>
<p>Inside the container:</p>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">exec</span> <span class="hljs-string">-it</span> <span class="hljs-string">traefik-demo</span> <span class="hljs-string">sh</span>
<span class="hljs-string">ls</span> <span class="hljs-string">-l</span> <span class="hljs-string">/letsencrypt/acme.json</span>
</code></pre>
<p>In logs:</p>
<pre><code class="lang-yaml"><span class="hljs-string">docker</span> <span class="hljs-string">logs</span> <span class="hljs-string">-f</span> <span class="hljs-string">traefik-demo</span> <span class="hljs-string">|</span> <span class="hljs-string">grep</span> <span class="hljs-string">-i</span> <span class="hljs-string">acme</span>
</code></pre>
<p>Look for:</p>
<ul>
<li><p>Certificates being requested / obtained</p>
</li>
<li><p>No “resolver skipped” or “challenge not enabled” errors</p>
</li>
</ul>
<h2 id="heading-why-this-architecture-makes-sense-for-n8n">Why This Architecture Makes Sense for n8n</h2>
<p>In the end, this setup gives you:</p>
<ul>
<li><p>A <strong>single, secure entrypoint</strong> (Traefik on ports 80/443).</p>
</li>
<li><p><strong>n8n hidden</strong> behind a Docker network (<code>frontend</code>), not exposed directly to the internet.</p>
</li>
<li><p><strong>Automatic, renewable HTTPS</strong> via Let’s Encrypt.</p>
</li>
<li><p><strong>Forced HTTPS</strong> (HTTP → HTTPS redirect).</p>
</li>
<li><p>Routing defined <strong>right next to the service</strong> (as Docker labels) instead of a giant reverse proxy config.</p>
</li>
<li><p>n8n correctly configured with its external URL and secured with Basic Auth.</p>
</li>
</ul>
<p>And the best part: when you want to add another service—say, an API at <a target="_blank" href="http://api.mahdishadi.me"><code>api.mahdishadi.me</code></a>—you just add another container with a few Traefik labels. Traefik discovers it automatically; no restarts, no big config edits..</p>
]]></content:encoded></item><item><title><![CDATA[Want Seamless, Scalable Storage in Kubernetes? Here’s Why Longhorn is Your Best Bet!]]></title><description><![CDATA[Longhorn: Scalable Storage for Kubernetes
What is Longhorn?
Imagine you're setting up a big project on Kubernetes and you need scalable storage that's both easy to use and reliable. That's where Longhorn comes in. Longhorn is a distributed storage sy...]]></description><link>https://blog.mahdishadi.me/want-seamless-scalable-storage-in-kubernetes-via-longhorn</link><guid isPermaLink="true">https://blog.mahdishadi.me/want-seamless-scalable-storage-in-kubernetes-via-longhorn</guid><category><![CDATA[longhorn]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[#kubernetes #container ]]></category><category><![CDATA[Traefik]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Wed, 15 Oct 2025 10:08:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760522738438/f7fc43e0-c696-42f4-8019-1cdb5a09dd09.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-longhorn-scalable-storage-for-kubernetes"><strong>Longhorn: Scalable Storage for Kubernetes</strong></h1>
<h2 id="heading-what-is-longhorn"><strong>What is Longhorn?</strong></h2>
<p>Imagine you're setting up a big project on <strong>Kubernetes</strong> and you need scalable storage that's both easy to use and reliable. That's where Longhorn comes in. Longhorn is a <strong>distributed storage system</strong> built specifically for Kubernetes. In simple terms, Longhorn is a cloud-based storage solution for Kubernetes that lets you easily store data in your cluster, in a distributed way, with high availability..</p>
<h2 id="heading-longhorn-structure"><strong>Longhorn Structure</strong></h2>
<p>Longhorn uses a distributed structure with a few main components:</p>
<ul>
<li><p><strong>Controller</strong>: Manages the cluster's state and coordinates between nodes.</p>
</li>
<li><p><strong>Replica</strong>: Stores copies of data. These replicas are spread across different nodes in the cluster.</p>
</li>
<li><p><strong>Engine</strong>: Processes requests and reads/writes data from various nodes.</p>
</li>
<li><p><strong>Driver</strong>: The main interface for interacting with other storage systems.</p>
</li>
</ul>
<p>This structure allows you to distribute data across multiple nodes, providing <strong>scalability</strong>, <strong>reliability</strong>, and <strong>high performance</strong> within Kubernetes.</p>
<hr />
<h2 id="heading-why-should-you-use-longhorn"><strong>Why Should You Use Longhorn?</strong></h2>
<p>When you're working with Kubernetes, one of the biggest challenges is <strong>data storage</strong>. Most traditional storage systems aren’t optimized for Kubernetes and come with problems like poor scalability, slow performance, and complex management.</p>
<p><strong>But Longhorn solves these problems:</strong></p>
<ol>
<li><p><strong>High Scalability</strong>: Longhorn can easily scale by adding new nodes, increasing your storage capacity.</p>
</li>
<li><p><strong>High Availability (HA)</strong>: If one of your nodes goes down, Longhorn can load the data from other nodes, so there's no problem.</p>
</li>
<li><p><strong>Easy Setup</strong>: Longhorn is designed for Kubernetes, so installation is simple and fast.</p>
</li>
</ol>
<p>If you're looking for a storage system with these features for your projects, Longhorn is one of the best options.</p>
<p><img src="https://longhorn.io/img/diagrams/architecture/how-longhorn-works.svg" alt /></p>
<hr />
<h2 id="heading-pre-requisites-for-installing-longhorn"><strong>Pre-requisites for Installing Longhorn</strong></h2>
<p>Before installing Longhorn on Kubernetes, make sure the following pre-requisites are installed on your cluster nodes. These tools will help Longhorn manage disks and storage resources properly.</p>
<h3 id="heading-1-open-iscsi"><strong>1. open-iscsi</strong></h3>
<p>Longhorn needs <strong>open-iscsi</strong> to connect to disks and storage resources. Install it like this:</p>
<pre><code class="lang-bash">sudo apt-get update
sudo apt-get install -y open-iscsi
sudo systemctl <span class="hljs-built_in">enable</span> --now iscsid
</code></pre>
<h3 id="heading-2-nfs-common"><strong>2. nfs-common</strong></h3>
<p>For NFSv4 support and to share data between nodes, you need <strong>nfs-common</strong>:</p>
<pre><code class="lang-bash">sudo apt-get update
sudo apt-get install -y nfs-common
</code></pre>
<h3 id="heading-3-cryptsetup"><strong>3. cryptsetup</strong></h3>
<p>If you want to encrypt disks, install <strong>cryptsetup</strong>:</p>
<pre><code class="lang-bash">sudo apt-get update
sudo apt-get install -y cryptsetup
</code></pre>
<h3 id="heading-4-device-mapper-persistent-data"><strong>4. device-mapper-persistent-data</strong></h3>
<p>To use <strong>LVM</strong> for disk management, install <strong>device-mapper-persistent-data</strong>:</p>
<pre><code class="lang-bash">sudo apt-get update
sudo apt-get install -y device-mapper-persistent-data
</code></pre>
<h3 id="heading-5-system-utilities"><strong>5. System Utilities</strong></h3>
<p>For managing the system and running various commands, you need utilities like <strong>bash</strong>, <strong>curl</strong>, <strong>findmnt</strong>, <strong>grep</strong>, etc.:</p>
<pre><code class="lang-bash">sudo apt-get update
sudo apt-get install -y bash curl findmnt grep awk blkid lsblk
</code></pre>
<hr />
<h2 id="heading-checking-prerequisites-with-longhornctl-script"><strong>Checking Prerequisites with longhornctl Script</strong></h2>
<p>When you want to install <strong>Longhorn</strong> on Kubernetes, you need to make sure all the necessary prerequisites are installed correctly. To make this easy, you can use the <strong>longhornctl</strong> script, which checks the status of your pre-requisites and tells you if everything's good or if something's missing.</p>
<h3 id="heading-installing-longhornctl"><strong>Installing longhornctl</strong></h3>
<p>First, you need to download <strong>longhornctl</strong>, which acts like a checker that verifies if the required pre-requisites are in place.</p>
<p>To install <strong>longhornctl</strong> on your system, run:</p>
<pre><code class="lang-bash">curl -sSfL -o longhornctl https://github.com/longhorn/cli/releases/download/v1.10.0/longhornctl-linux-amd64
chmod +x longhornctl
</code></pre>
<h3 id="heading-checking-prerequisites-with-longhornctl"><strong>Checking Prerequisites with longhornctl</strong></h3>
<p>Now that you've installed <strong>longhornctl</strong>, you can easily use it to check if the prerequisites for <strong>Longhorn</strong> are installed properly.</p>
<p>Just run this command:</p>
<pre><code class="lang-bash">./longhornctl check preflight
</code></pre>
<p>This will automatically check if things like <strong>open-iscsi</strong>, <strong>nfs-common</strong>, <strong>cryptsetup</strong>, and other needed tools are installed and running.</p>
<h3 id="heading-how-will-the-script-output-look"><strong>How Will the Script Output Look?</strong></h3>
<p>If everything is installed correctly, the output will look something like this:</p>
<pre><code class="lang-bash">Checking prerequisites...
- [PASS] open-iscsi: Installed and running
- [PASS] nfs-common: Installed
- [PASS] cryptsetup: Installed
- [PASS] device-mapper-persistent-data: Installed
</code></pre>
<p>But if any of the prerequisites are missing or not working properly, <strong>longhornctl</strong> will let you know what the problem is and what action to take.</p>
<hr />
<h2 id="heading-installing-longhorn-on-kubernetes"><strong>Installing Longhorn on Kubernetes</strong></h2>
<p>Now that the prerequisites are installed, it's time to install <strong>Longhorn</strong> on Kubernetes.</p>
<h3 id="heading-1-add-the-helm-repository"><strong>1. Add the Helm Repository</strong></h3>
<p>First, add the <strong>Helm</strong> repository for Longhorn:</p>
<pre><code class="lang-bash">helm repo add longhorn https://charts.longhorn.io
helm repo update
</code></pre>
<h3 id="heading-2-create-a-namespace-for-longhorn"><strong>2. Create a Namespace for Longhorn</strong></h3>
<p>To manage Longhorn, create a <code>longhorn-system</code> namespace:</p>
<pre><code class="lang-bash">kubectl create namespace longhorn-system
</code></pre>
<h3 id="heading-3-install-longhorn-using-helm"><strong>3. Install Longhorn Using Helm</strong></h3>
<p>For the first create yaml file to ensure Longhorn Replicas:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">longhornUI:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
</code></pre>
<p>Now, install Longhorn using the following command:</p>
<pre><code class="lang-bash">helm install longhorn longhorn/longhorn -n longhorn-system --values longhorn.yaml
</code></pre>
<h3 id="heading-4-check-installation-status"><strong>4. Check Installation Status</strong></h3>
<p>To ensure Longhorn is installed correctly, check the pods:</p>
<pre><code class="lang-bash">kubectl -n longhorn-system get pods
</code></pre>
<hr />
<h1 id="heading-what-i-did">What i did:</h1>
<p><strong>Why Couldn’t I Access Longhorn from Outside Without These Configurations?</strong></p>
<p>When you install <strong>Longhorn</strong>, it’s basically a service running inside your <strong>Kubernetes</strong> cluster, and by default, it’s only accessible from inside the cluster. So, if you want to access the Longhorn dashboard from outside the cluster (for example, from your browser), you won’t be able to, because Kubernetes, by default, only exposes services of type <strong>ClusterIP</strong> internally.</p>
<p>To make services like <strong>Longhorn</strong> accessible from outside the cluster, we need either a <strong>Load Balancer</strong> or an <strong>Ingress Controller</strong>. That's where <strong>Traefik</strong> comes in.</p>
<h2 id="heading-why-did-i-use-traefik"><strong>Why Did I Use Traefik?</strong></h2>
<p>In <strong>Kubernetes</strong>, if we want to route external traffic into the cluster, we need an <strong>Ingress Controller</strong>. <strong>Traefik</strong> is one of the best options for this job. Here’s why I used <strong>Traefik</strong>:</p>
<ol>
<li><p><strong>Excellent Integration with Kubernetes</strong>: Traefik works natively with Kubernetes, automatically detecting and routing traffic to the right services inside the cluster.</p>
</li>
<li><p><strong>SSL/TLS Support</strong>: Traefik can automatically redirect traffic from <strong>HTTP</strong> to <strong>HTTPS</strong> and can get SSL certificates from <strong>Let’s Encrypt</strong> without much hassle.</p>
</li>
<li><p><strong>Auto Discovery</strong>: When a new service is added, Traefik automatically detects it, so we don’t need to manually update the configuration.</p>
</li>
<li><p><strong>Scalability</strong>: Traefik can balance traffic between multiple instances of the same service, making it easy to scale as needed.</p>
</li>
</ol>
<p>So, to access <strong>Longhorn</strong> from outside the cluster, I needed an <strong>Ingress Controller</strong>, and <strong>Traefik</strong> was the perfect choice for this.</p>
<h2 id="heading-why-did-i-add-ssl-to-traefik-and-longhorn"><strong>Why Did I Add SSL to Traefik and Longhorn?</strong></h2>
<p>Now that we're accessing the <strong>Longhorn</strong> service from outside the cluster, it's important to ensure the connection is secure. This is where <strong>SSL/TLS</strong> comes in.</p>
<h3 id="heading-why-ssl"><strong>Why SSL?</strong></h3>
<ul>
<li><p><strong>Security</strong>: Without SSL, any data transmitted between the user and the server can be intercepted. SSL ensures that the connection is encrypted, protecting the data from <strong>Man-in-the-Middle (MITM)</strong> attacks.</p>
</li>
<li><p><strong>Encryption</strong>: SSL ensures that communications with <strong>Longhorn</strong> and any other services inside Kubernetes are encrypted, making the connection safe.</p>
</li>
<li><p><strong>SEO</strong>: When a site or service has SSL, search engines like Google trust it more. This gives <strong>Longhorn</strong> the security and credibility it needs.</p>
</li>
</ul>
<h2 id="heading-ssl-with-traefik-technical-overview"><strong>SSL with Traefik: Technical Overview</strong></h2>
<ul>
<li><p><strong>Let’s Encrypt</strong>: I used <strong>Let’s Encrypt</strong>, which automatically issues SSL certificates, making it super easy to implement secure communication without manual certificate management.</p>
</li>
<li><p><strong>SSL Termination</strong>: With Traefik, SSL termination happens at the Traefik level, meaning SSL is handled by Traefik, and then the traffic is sent as HTTP inside the cluster. This simplifies the overall architecture and offloads SSL work from the backend services.</p>
</li>
</ul>
<h2 id="heading-why-couldnt-i-access-longhorn-without-this-configuration"><strong>Why Couldn’t I Access Longhorn Without This Configuration?</strong></h2>
<p>When you install <strong>Longhorn</strong>, it’s a <strong>ClusterIP</strong> service by default, which means it’s only accessible within the cluster. To expose it to the outside world, you need either a <strong>LoadBalancer</strong> or an <strong>Ingress Controller</strong> like Traefik.</p>
<p>By default, Kubernetes only exposes services with <strong>ClusterIP</strong> type internally. So, without using <strong>Ingress</strong> or <strong>LoadBalancer</strong>, no one from outside can access your internal services like Longhorn.</p>
<h3 id="heading-how-does-traefik-solve-this"><strong>How Does Traefik Solve This?</strong></h3>
<ul>
<li><p><strong>IngressRoute</strong> in <strong>Traefik</strong> allows us to route incoming traffic from outside the cluster to internal services like <strong>Longhorn</strong>.</p>
</li>
<li><p><strong>Traefik</strong> automatically detects services and routes traffic accordingly. This means that we can access <strong>Longhorn</strong> from outside the cluster once we configure <strong>Traefik</strong> correctly.</p>
</li>
</ul>
<h3 id="heading-what-makes-longhorn-accessible-from-outside-the-cluster">🔧 <strong>What Makes Longhorn Accessible from Outside the Cluster?</strong></h3>
<p>In the end, by using <strong>Traefik</strong> as an <strong>Ingress Controller</strong>, I was able to route incoming traffic from outside the cluster to <strong>Longhorn</strong>. This made accessing the <strong>Longhorn</strong> dashboard easy and secure using <strong>SSL</strong>.</p>
<h3 id="heading-heres-how-it-worked"><strong>Here’s How It Worked:</strong></h3>
<ol>
<li><p>I created an <strong>IngressRoute</strong> in <strong>Traefik</strong>, telling it to route traffic with the <strong>Host</strong> <a target="_blank" href="http://longhorn.mahdishadi.me"><code>longhorn.mahdishadi.me</code></a> to the <strong>longhorn-frontend</strong> service inside the cluster.</p>
</li>
<li><p>I set up <strong>SSL</strong> for security, ensuring all traffic is redirected from HTTP to HTTPS.</p>
</li>
<li><p>Traefik automatically fetched an SSL certificate from <strong>Let’s Encrypt</strong>.</p>
</li>
</ol>
<h2 id="heading-using-https-with-traefik"><strong>Using HTTPS with Traefik</strong></h2>
<p>To make sure access to Longhorn from outside Kubernetes is secure (HTTPS), you need to use <strong>Traefik</strong>. Here's how to configure SSL and IngressRoute:</p>
<h3 id="heading-1-configuring-traefik-for-http-https-redirect-amp-ingressroute-for-longhorn-access"><strong>1. Configuring Traefik for HTTP → HTTPS Redirect &amp; IngressRoute for Longhorn Access</strong></h3>
<p>To redirect HTTP requests to HTTPS, create a middleware called <strong>https-redirect</strong> and to configure the <strong>IngressRoute</strong> via Traefik, make sure requests go to port 80:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">traefik.io/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Middleware</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">https-redirect</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">longhorn-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">redirectScheme:</span>
    <span class="hljs-attr">scheme:</span> <span class="hljs-string">https</span>
    <span class="hljs-attr">permanent:</span> <span class="hljs-literal">true</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">traefik.io/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">IngressRoute</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">longhorn-web-http</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">longhorn-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">entryPoints:</span> [<span class="hljs-string">web</span>]
  <span class="hljs-attr">routes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">match:</span> <span class="hljs-string">Host(`longhorn.mahdishadi.me`)</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">Rule</span>
    <span class="hljs-attr">middlewares:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">https-redirect</span>
    <span class="hljs-attr">services:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">longhorn-frontend</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
</code></pre>
<h3 id="heading-2-configuring-ssl-with-cert-manager"><strong>2. Configuring SSL with cert-manager</strong></h3>
<p>To get an SSL certificate from <strong>Let’s Encrypt</strong>, configure a <strong>ClusterIssuer</strong>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">cert-manager.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterIssuer</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">letsencrypt-http01</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">acme:</span>
    <span class="hljs-attr">email:</span> <span class="hljs-string">mahdishadi99@gmail.com</span>
    <span class="hljs-attr">server:</span> <span class="hljs-string">https://acme-v02.api.letsencrypt.org/directory</span>
    <span class="hljs-attr">privateKeySecretRef:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">letsencrypt-account-key</span>
    <span class="hljs-attr">solvers:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">http01:</span>
        <span class="hljs-attr">ingress:</span>
          <span class="hljs-attr">class:</span> <span class="hljs-string">traefik</span>
</code></pre>
<p>At the end, you can see the UI on the web browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760517117647/4d854d81-4f27-4709-9ab2-0dba758d54df.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-explain-what-pv-pvc-and-storageclass-about">Explain What PV, PVC, and StorageClass about:</h1>
<p>When real-world apps are involved, “disk” is no joke. That’s why Kubernetes supports multiple storage models—block, file, and object—whether you’re on the cloud or in your own data center.</p>
<h2 id="heading-what-are-the-plugin-layer-and-csi">What Are the Plugin Layer and CSI?</h2>
<p>There’s a plugin layer in the middle that brokers between Kubernetes and external storage systems. Modern plugins speak <strong>Container Storage Interface (CSI)</strong>—an open standard that lets storage drivers behave consistently across orchestration platforms like K8s.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760516569045/785f4031-d0f9-4233-84fe-c75ae4994f80.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-core-trio-pv-pvc-and-storageclass">The Core Trio: PV, PVC, and StorageClass</h2>
<ul>
<li><p><strong>PersistentVolume (PV):</strong> The in-cluster handle (representation) of an external volume.</p>
</li>
<li><p><strong>PersistentVolumeClaim (PVC):</strong> A pod’s request to use a PV.</p>
</li>
<li><p><strong>StorageClass (SC):</strong> The “plan/tier” definition that automates creating PVs and backend volumes.</p>
</li>
</ul>
<p>In plain English: <strong>PV</strong> is the map to the external volume, <strong>PVC</strong> is the permission slip to use it, and <strong>SC</strong> makes the whole thing <strong>dynamic and automatic</strong>.</p>
<h2 id="heading-a-step-by-step-scenario-longhorn-50gb">A Step-by-Step Scenario (Longhorn – 50GB)</h2>
<ol>
<li><p>A Pod needs 50GB ⇒ it creates a <strong>PVC</strong>.</p>
</li>
<li><p>The <strong>PVC</strong> asks the <strong>StorageClass</strong> to create a new PV + backend volume.</p>
</li>
<li><p>The <strong>SC</strong> calls the Longhorn backend via the Longhorn <strong>CSI</strong> driver.</p>
</li>
<li><p>The <strong>CSI</strong> driver creates a <strong>50GB volume</strong> on Longhorn .</p>
</li>
<li><p><strong>CSI</strong> reports the external volume is ready.</p>
</li>
<li><p>The <strong>SC</strong> creates a <strong>PV</strong> and maps it to that Longhorn volume.</p>
</li>
<li><p>The Pod mounts the PV and starts using it.</p>
</li>
</ol>
<blockquote>
<p>Safety note: K8s prevents multiple pods from writing to the same PV willy-nilly. Also, PVs have a <strong>1:1</strong> relationship with external volumes; you can’t split a single 50GB volume into two 25GB PVs.</p>
</blockquote>
<h2 id="heading-how-do-providersprovisioners-get-added">How Do Providers/Provisioners Get Added?</h2>
<p>Each storage provider usually ships a <strong>CSI driver</strong> via <strong>Helm</strong> or a YAML installer. After installation, the driver’s pods run in the <strong>kube-system</strong> namespace and are ready to serve.</p>
<h2 id="heading-the-persistent-volume-subsystem-in-practice">The Persistent Volume Subsystem in Practice</h2>
<p>Say your external storage exposes these tiers:</p>
<ul>
<li><p><strong>Fast block (flash)</strong></p>
</li>
<li><p><strong>Fast encrypted block (flash)</strong></p>
</li>
<li><p><strong>Slow block (mechanical)</strong></p>
</li>
<li><p><strong>File (NFS)</strong></p>
</li>
</ul>
<p>You create one <strong>StorageClass</strong> per tier so apps can request exactly what they need. If a new app requires 100GB of <strong>encrypted flash</strong>, define a <strong>PVC</strong> in your Pod’s YAML that asks the <strong>sc-fast-encrypted</strong> class for 100GB.</p>
<p>When you apply the manifest, the <strong>SC controller</strong> notices the new PVC and tells the <strong>CSI</strong> driver to provision 100GB of encrypted flash. The external system creates the volume and reports back; CSI informs the SC controller, which maps it to a new <strong>PV</strong>. The PVC then binds to that PV, and your Pod mounts it.</p>
<h2 id="heading-handy-yaml-tips">Handy YAML Tips</h2>
<ul>
<li><p><code>apiVersion</code> and <code>kind</code> declare the object’s type and API version.</p>
</li>
<li><p><a target="_blank" href="http://metadata.name"><code>metadata.name</code></a> is a friendly identifier.</p>
</li>
<li><p>In a <strong>StorageClass</strong>, the <code>provisioner</code> field selects the CSI driver.</p>
</li>
<li><p><strong>StorageClasses are immutable</strong>; if you misconfigure one, create a new one.</p>
</li>
<li><p>People often use <strong>provisioner / plugin / driver</strong> interchangeably.</p>
</li>
<li><p>The <code>parameters</code> block is driver-specific (varies per CSI).</p>
</li>
</ul>
<h2 id="heading-access-modes-how-a-volume-can-be-used">Access Modes (How a Volume Can Be Used)</h2>
<p>Kubernetes supports three:</p>
<ul>
<li><p><strong>ReadWriteOnce (RWO):</strong> One PVC can mount R/W (often from a single node).</p>
</li>
<li><p><strong>ReadWriteMany (RWX):</strong> Multiple PVCs can mount R/W (typically file/object like NFS; block rarely supports this).</p>
</li>
<li><p><strong>ReadOnlyMany (ROX):</strong> Multiple PVCs can mount read-only.</p>
</li>
</ul>
<blockquote>
<p>A PV can be opened in <strong>only one mode</strong> at a time; you can’t mount the same PV as ROX for one PVC and RWX for another simultaneously.</p>
</blockquote>
<h2 id="heading-reclaim-policy-what-happens-when-the-pvc-is-deleted">Reclaim Policy (What Happens When the PVC Is Deleted?)</h2>
<ul>
<li><p><strong>Delete</strong> (default for dynamically created PVs): deleting the PVC deletes the PV <strong>and</strong> the external volume. Risky if you lack backups!</p>
</li>
<li><p><strong>Retain:</strong> keeps the PV and external volume after the PVC is deleted—you’ll clean things up <strong>manually</strong>. Safer.</p>
</li>
</ul>
<h2 id="heading-volumebindingmode-amp-topology-waitforfirstconsumer">VolumeBindingMode &amp; Topology (WaitForFirstConsumer)</h2>
<p>If you set <code>volumeBindingMode: WaitForFirstConsumer</code> in your <strong>StorageClass</strong>, the system waits to create the volume until a <strong>real Pod</strong> that uses the PVC is scheduled. The result? The volume is provisioned in the <strong>same region/zone</strong> as the Pod—avoiding cross-zone/region latency and costs.</p>
<hr />
<p>Alright—let’s wire Longhorn up to your Pods’ PVCs once and for all. You can drop this straight into your article—the tone’s casual, and the YAML is copy-paste ready 👇</p>
<h1 id="heading-create-a-pvc-with-longhorn">Create a PVC with Longhorn</h1>
<h2 id="heading-1-prereq-longhorn-should-be-up-and-healthy">1) Prereq: Longhorn should be up and healthy</h2>
<p>First, make sure Longhorn is installed and its Pods are healthy:</p>
<pre><code class="lang-plaintext">kubectl -n longhorn-system get pods
</code></pre>
<p>If everything is <code>Running</code>, move on. If not, fix Longhorn first, then come back.</p>
<h2 id="heading-2-create-a-storageclass-if-you-dont-have-one">2) Create a StorageClass (if you don’t have one)</h2>
<p>Longhorn usually ships a default StorageClass named <code>longhorn</code>. If you want custom settings (like replica count), create one like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">storage.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">StorageClass</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">longhorn-sc</span>
<span class="hljs-attr">provisioner:</span> <span class="hljs-string">driver.longhorn.io</span>
<span class="hljs-attr">allowVolumeExpansion:</span> <span class="hljs-literal">true</span>   <span class="hljs-comment"># so you can grow the volume later</span>
<span class="hljs-attr">reclaimPolicy:</span> <span class="hljs-string">Delete</span>        <span class="hljs-comment"># delete PV when PVC is deleted (tweak as needed)</span>
<span class="hljs-attr">parameters:</span>
  <span class="hljs-attr">numberOfReplicas:</span> <span class="hljs-string">"3"</span>      <span class="hljs-comment"># how many copies across nodes</span>
  <span class="hljs-attr">staleReplicaTimeout:</span> <span class="hljs-string">"30"</span>  <span class="hljs-comment"># minutes; remove slow/stale replicas</span>
  <span class="hljs-attr">fsType:</span> <span class="hljs-string">"ext4"</span>             <span class="hljs-comment"># filesystem on the volume</span>
</code></pre>
<p>Apply it:</p>
<pre><code class="lang-plaintext">kubectl apply -f longhorn-sc.yaml
</code></pre>
<h2 id="heading-3-create-the-persistentvolumeclaim-pvc">3) Create the PersistentVolumeClaim (PVC)</h2>
<p>Time to request some space. This example asks for 10Gi with RWO:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolumeClaim</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">my-pvc</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">accessModes:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>            <span class="hljs-comment"># single-node access; fits most apps</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">storage:</span> <span class="hljs-string">10Gi</span>
  <span class="hljs-attr">storageClassName:</span> <span class="hljs-string">longhorn-sc</span>  <span class="hljs-comment"># or use 'longhorn' if you want the default</span>
</code></pre>
<p>Apply it and check:</p>
<pre><code class="lang-plaintext">kubectl apply -f my-pvc.yaml
kubectl get pvc my-pvc
</code></pre>
<p>If <code>STATUS</code> is <code>Bound</code>, your volume is ready.</p>
<p>Now attach that storage to your container.</p>
<h2 id="heading-5-handy-tips-that-matter">5) Handy tips that matter</h2>
<ul>
<li><p><strong>RWO vs RWX</strong>: RWO works for most cases. For multi-node sharing (RWX), use Longhorn’s Share Manager (you’ll need an RWX-capable StorageClass).</p>
</li>
<li><p><strong>Expanding size</strong>: With <code>allowVolumeExpansion: true</code>, you can grow a PVC later (shrinking isn’t supported). Some apps may need a restart to see the new size.</p>
</li>
<li><p><strong>Troubleshooting</strong> <code>Pending</code>:</p>
<ul>
<li><p>Run <code>kubectl describe pvc my-pvc</code> to see what’s blocking.</p>
</li>
<li><p>Check node/disk capacity.</p>
</li>
<li><p>Inspect Longhorn components: <code>kubectl -n longhorn-system get pods</code> and look at the Longhorn Manager/CSI logs.</p>
</li>
</ul>
</li>
<li><p><strong>Replicas</strong>: More replicas = better resilience to node failure, but higher disk usage. Three is a safe bet.</p>
</li>
</ul>
<h2 id="heading-6-overview">6) Overview</h2>
<ol>
<li><p>Longhorn healthy? ✔️</p>
</li>
<li><p>Create (or use) a StorageClass (<code>longhorn</code> or your custom one) ✔️</p>
</li>
<li><p>Define a PVC and mount it in your Pod ✔️</p>
</li>
<li><p>If it’s <code>Pending</code>, describe the PVC and check logs ✔️</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Zero-Downtime Zabbix Migration: How I Moved from 6.4 to 7.0 Without Losing a Single Metric]]></title><description><![CDATA[Overall Architecture: Why It Works
Zabbix Server on Server 2 is the brain of the operation: it caches configuration, processes triggers, and writes data into the database.Frontend (running on Apache) provides dashboards, graphs, and configuration UI....]]></description><link>https://blog.mahdishadi.me/zero-downtime-zabbix-migration</link><guid isPermaLink="true">https://blog.mahdishadi.me/zero-downtime-zabbix-migration</guid><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Tue, 07 Oct 2025 10:56:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759834320484/e9a9b2f9-19a2-4d08-9cb4-768a35acbae7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-overall-architecture-why-it-works"><strong>Overall Architecture: Why It Works</strong></h2>
<p><strong>Zabbix Server</strong> on Server 2 is the brain of the operation: it caches configuration, processes triggers, and writes data into the database.<br /><strong>Frontend</strong> (running on Apache) provides dashboards, graphs, and configuration UI.<br /><strong>Database</strong> (MySQL/MariaDB) stores history, trends, and metadata.<br /><strong>Zabbix Proxies</strong> sit across sites — while the main server is cut over, they buffer data and later push it in bulk. That’s what makes the whole migration safe.</p>
<hr />
<h2 id="heading-before-starting-backup-and-freeze"><strong>Before Starting: Backup and Freeze</strong></h2>
<p>I scheduled a short <strong>data freeze</strong> to ensure nothing was written during the dump. Then I took a <strong>clean backup</strong> of the Zabbix database and temporarily stopped the old server — so the dump wouldn’t get interrupted.<br />Proxies kept collecting data in their buffers, waiting to sync afterward.</p>
<blockquote>
<p>Be sure to take snapshots of your machines and make sure you have backups before you start.</p>
</blockquote>
<pre><code class="lang-bash"><span class="hljs-comment"># consistent backup + clean handoff</span>
mysqldump --single-transaction -u root -p zabbix &gt; /tmp/zabbix_$(date +%F).sql
sudo systemctl stop zabbix-server

<span class="hljs-comment"># also save configs for safety</span>
<span class="hljs-comment"># /etc/zabbix/* and /etc/zabbix/web/zabbix.conf.php (if local)</span>
</code></pre>
<hr />
<h2 id="heading-building-server-2-apache-php-8-database-zabbix-70"><strong>Building Server 2: Apache + PHP 8 + Database + Zabbix 7.0</strong></h2>
<p>I built the new environment in order: web stack → database → Zabbix.<br />If you get the classic <code>ERROR 2002 (socket)</code> while connecting to MySQL, it simply means the MySQL service isn’t installed or running.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># web stack</span>
sudo apt update
sudo apt install -y apache2 php php-{mysql,mbstring,xml,bcmath,gd,curl}

<span class="hljs-comment"># DB server</span>
sudo apt install -y mysql-server
sudo systemctl <span class="hljs-built_in">enable</span> --now mysql
sudo mysql   <span class="hljs-comment"># enter MySQL shell (Ubuntu uses socket auth)</span>
</code></pre>
<p>Create a dedicated Zabbix database with <strong>utf8mb4_bin</strong> to avoid Unicode comparison issues, and a user with proper privileges:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> zabbix <span class="hljs-built_in">CHARACTER</span> <span class="hljs-keyword">SET</span> utf8mb4 <span class="hljs-keyword">COLLATE</span> utf8mb4_bin;
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> <span class="hljs-string">'zabbix'</span>@<span class="hljs-string">'localhost'</span> <span class="hljs-keyword">IDENTIFIED</span> <span class="hljs-keyword">BY</span> <span class="hljs-string">'StrongPass!'</span>;
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">ON</span> zabbix.* <span class="hljs-keyword">TO</span> <span class="hljs-string">'zabbix'</span>@<span class="hljs-string">'localhost'</span>;
<span class="hljs-keyword">FLUSH</span> <span class="hljs-keyword">PRIVILEGES</span>;
</code></pre>
<p>Then add the <strong>official Zabbix 7.0 repository</strong> and install the packages:</p>
<pre><code class="lang-bash">wget https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest_7.0+ubuntu24.04_all.deb
sudo dpkg -i zabbix-release_latest_7.0+ubuntu24.04_all.deb
sudo apt update
sudo apt install zabbix-server-mysql zabbix-frontend-php zabbix-apache-conf zabbix-sql-scripts zabbix-agent2
sudo apt install zabbix-agent2-plugin-mongodb zabbix-agent2-plugin-mssql zabbix-agent2-plugin-postgresql
</code></pre>
<hr />
<h2 id="heading-restoring-data-and-connecting-services"><strong>Restoring Data and Connecting Services</strong></h2>
<p>Once the database was ready, I restored the dump and pointed both the <strong>server</strong> and <strong>frontend</strong> to it. If you skip this carefully, you’ll face DB/frontend mismatches later.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># bring back your history and config</span>
mysql -u root -p zabbix &lt; /tmp/zabbix_YYYY-MM-DD.sql

<span class="hljs-comment"># configure DB parameters for the Zabbix server</span>
sudo nano /etc/zabbix/zabbix_server.conf
<span class="hljs-comment"># DBName=zabbix</span>
<span class="hljs-comment"># DBUser=zabbix</span>
<span class="hljs-comment"># DBPassword=StrongPass!</span>
<span class="hljs-comment"># DBHost=127.0.0.1</span>

<span class="hljs-comment"># enable frontend configuration for Apache</span>
sudo a2enconf zabbix-frontend-php
sudo systemctl reload apache2
</code></pre>
<blockquote>
<p>Don't forget to change the IP on the second server back to the previous Zabbix IP or apply your changes to DNS!</p>
</blockquote>
<p>When you start Zabbix 7.0, it automatically upgrades the schema. I monitored the logs during this step.<br />If the UI complains like “DB version 6050035, required 7000000,” it means the new server hasn’t fully upgraded the DB yet — or your <strong>binlog trust settings</strong> are blocking it.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># start services and watch logs</span>
sudo systemctl <span class="hljs-built_in">enable</span> --now zabbix-server zabbix-agent apache2
sudo tail -f /var/<span class="hljs-built_in">log</span>/zabbix/zabbix_server.log

<span class="hljs-comment"># if mismatch: verify versions and DB targets, then temporarily trust function creators</span>
zabbix_server -V
grep -E <span class="hljs-string">'DB(Name|User|Host)'</span> /etc/zabbix/zabbix_server.conf
grep -E <span class="hljs-string">'DB(Name|User|Host)'</span> /etc/zabbix/web/zabbix.conf.php
sudo mysql -e <span class="hljs-string">"SET GLOBAL log_bin_trust_function_creators=1;"</span>
sudo systemctl restart zabbix-server
<span class="hljs-comment"># revert after upgrade</span>
sudo mysql -e <span class="hljs-string">"SET GLOBAL log_bin_trust_function_creators=0;"</span>
</code></pre>
<h2 id="heading-breathing-room-for-the-caches"><strong>Breathing Room for the Caches</strong></h2>
<p>For a busy infrastructure, <strong>CacheSize=32M</strong> is a joke. I bumped it to 1 GB (2–4 GB for very large environments). Then I verified <code>/dev/shm</code> was big enough for shared memory — ideally double the cache size. Using the internal item <code>zabbix[rcache,buffer,pused]</code>, I kept cache utilization between 40–60%. Anything above 70% meant it was time to increase it again.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># give config cache real headroom</span>
sudo sed -i <span class="hljs-string">'s/^#\?CacheSize=.*/CacheSize=1024M/'</span> /etc/zabbix/zabbix_server.conf
sudo systemctl restart zabbix-server
sudo tail -f /var/<span class="hljs-built_in">log</span>/zabbix/zabbix_server.log

<span class="hljs-comment"># ensure shared memory is large enough (≥ 2× CacheSize)</span>
df -h /dev/shm
</code></pre>
<h2 id="heading-post-cutover-validation"><strong>Post-Cutover Validation</strong></h2>
<p>Once the cutover was done, I confirmed all <strong>Proxies were Connected</strong>, <strong>Latest Data</strong> was flowing, and <strong>media/actions</strong> worked correctly.<br />If the UI complained about missing PHP extensions or timezone issues, fixing them and reloading Apache solved it quickly.</p>
<p>These small checks make the following day calm and predictable.</p>
<blockquote>
<p><strong>Rollback tip:</strong> If you ever need to revert, just stop services on Server 2, restore the pre-migration backup, point DNS back to Server 1, and review logs before retrying.</p>
</blockquote>
<h3 id="heading-final-thoughts"><strong>Final Thoughts</strong></h3>
<p>This migration reinforced one core lesson: <strong>a Zabbix upgrade isn’t hard if your proxies and database hygiene are solid</strong>. Plan your freeze, verify each step, and keep an eye on logs — that’s 90% of the job.</p>
]]></content:encoded></item><item><title><![CDATA[Monitoring internet Speed with Zabbix and Grafana]]></title><description><![CDATA[Zabbix is an open-source monitoring software tool used for monitoring diverse IT components, including networks, servers, virtual machines, and cloud services. It's designed to provide real-time insights into the performance and availability of vario...]]></description><link>https://blog.mahdishadi.me/monitoring-internet-speed-with-zabbix-and-grafana</link><guid isPermaLink="true">https://blog.mahdishadi.me/monitoring-internet-speed-with-zabbix-and-grafana</guid><category><![CDATA[Zabbix]]></category><category><![CDATA[Grafana]]></category><category><![CDATA[Grafana Monitoring]]></category><category><![CDATA[speedtest]]></category><category><![CDATA[internet]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Thu, 18 Apr 2024 09:42:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713433419811/b3af6c91-c8e1-4c69-81e8-05cceec66da5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Zabbix is an open-source monitoring software tool used for monitoring diverse IT components, including networks, servers, virtual machines, and cloud services. It's designed to provide real-time insights into the performance and availability of various components in your infrastructure.</p>
<p>monitoring internet speed and Quality is one of the important things that Companies need. in this Article, we want to describe how we can monitor internet speed using Zabbix.</p>
<p>In this Article , I use Centos Version 8 and Zabbix 6.4.10.</p>
<h2 id="heading-install-dependencies">Install dependencies</h2>
<p>first, we need to install dependencies.</p>
<p>dependencies Contain git, curl, wget, Zabbix agent2 and speedtest</p>
<pre><code class="lang-plaintext">yum update
yum install git -y

rpm -Uvh https://repo.zabbix.com/zabbix/6.4/rhel/8/x86_64/zabbix-release-6.4-1.el8.noarch.rpm
dnf clean all
dnf install zabbix-agent2 zabbix-agent2-plugin-* -y
yum install zabbix-sender -y

curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
yum install speedtest -y
</code></pre>
<p>open /etc/zabbix/zabbix_agent2.conf file with nano and add your Zabbix IP and your server Hostname for add to Zabbix</p>
<pre><code class="lang-plaintext">Server=YOUR_ZABBIX_IP
ServerActive=YOUR_ZABBIX_IP
Hostname=YOUR_SERVER_NAME
</code></pre>
<p>after that restart Zabbix Agent2</p>
<pre><code class="lang-plaintext">systemctl restart zabbix-agent2
systemctl enable zabbix-agent2
</code></pre>
<p>Now you can add your server to your Zabbix Server</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713420786651/45673ad8-9721-49a6-8164-df175b59eb31.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-clone-the-git-repository">Clone the git Repository</h2>
<p>then, clone the GitHub repository with the blow Command.</p>
<pre><code class="lang-plaintext">git clone https://github.com/soloranger/zabbix-internet-Speedtest-Template.git
</code></pre>
<p>cd to Directory and add <a target="_blank" href="https://github.com/soloranger/zabbix-internet-Speedtest-Template/blob/main/Speedtest_Template.xml">Speedtest_Template.xml</a> Z<a target="_blank" href="https://github.com/soloranger/zabbix-internet-Speedtest-Template/blob/main/Speedtest_Template.xml">abbix Template from</a> Data Collection &gt; Templates &gt; import</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713425288052/6c1ac319-b052-4a4c-80b3-61a081a89105.png" alt class="image--center mx-auto" /></p>
<p>cd to Code Directory and run speedtest.sh with -s argument. -s argument for the server ID that you want to use for the speed test.</p>
<pre><code class="lang-bash">chmod +x speedtest.sh
./speedtest -s 58210
</code></pre>
<p>after a few seconds, You can see Zabbix_Sender, Send data to Zabbix</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713430771078/c831332b-ec4b-4d9d-a7fc-eb53094aa867.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-grafana-dashboard">Grafana Dashboard</h2>
<p>It's time to see the output in a beautiful dashboard in Grafana :D</p>
<p>open Grafana -&gt; Click + icon -&gt; select import dashboard -&gt; select Config.json file from Git Repository</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713432443794/0468fcd1-0be9-4497-a323-44b5f9161ef4.png" alt class="image--center mx-auto" /></p>
<p>Notice: You should Connect your Zabbix to your Grafana.</p>
<p>after added, you can see your dashboard with this Data :)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713432514398/7ae548a6-7b08-4ed2-b4ee-5daa8312f55d.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Everything about SDDC]]></title><description><![CDATA[Migrating to the Cloud has brought unprecedented flexibility to today's companies. However, since today's businesses use these benefits more, they risk creating unwanted complexity in the data center.
To solve this problem, SDDC integrates virtualize...]]></description><link>https://blog.mahdishadi.me/everything-about-sddc</link><guid isPermaLink="true">https://blog.mahdishadi.me/everything-about-sddc</guid><category><![CDATA[SDDC]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[virtualization]]></category><category><![CDATA[storage]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Thu, 11 Apr 2024 13:36:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712842476931/e4a66448-214d-4a1f-9462-60fadbc16fe4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Migrating to the Cloud has brought unprecedented flexibility to today's companies. However, since today's businesses use these benefits more, they risk creating unwanted complexity in the data center.</p>
<p>To solve this problem, SDDC integrates virtualized infrastructure and simplifies resource provisioning and management. When successfully implemented, it is an integrated architecture that considers and coordinates all essential components of the data center.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712841141838/eef44e40-8cb5-4a77-a018-7fff24f15387.png" alt class="image--center mx-auto" /></p>
<p>To understand the benefits and challenges surrounding SDDC, we need to consider how we got here</p>
<h2 id="heading-traditional-data-center-vs-virtualization"><strong>Traditional Data Center vs Virtualization:</strong></h2>
<p>In its old form, the data center consists of three pillars of IT infrastructure: computing resources, storage, and networking</p>
<p>Once upon a time, all three were physical resources that were often geographically co-located—for some organizations, this is still true. However, the vast majority of companies have moved at least part of their infrastructure to the cloud and Some of the resources are virtualized due to scalability, flexibility, and cost-effectiveness. Today, virtual resources may take many forms.</p>
<h2 id="heading-sddc-architecture"><strong>SDDC  Architecture:</strong></h2>
<p>The SDDC architecture represents a complex approach to data center management and has multiple layers that focus on different functions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712841277843/a6930215-c4db-423b-b4ec-32fcd391ee9f.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>Physical layer:</strong> Computing, storage, and network devices in the data center are placed in this layer. This layer focuses on the performance and operational stability of the devices and provides a stable environment for the entire network and SDDC business operations.</p>
<p><strong>Virtual layer:</strong> Controls access to the physical infrastructure and segregates resources to provide them as services. It is also responsible for monitoring network operations and resource allocation, simplifying data center management, and improving efficiency.</p>
<p>management layer: standardizes management and enables orchestration and automation capabilities, allowing SDDC to be controlled from a central point.</p>
<p>To better understand SDDC, it is better to learn more about the concepts of SDN and SDS.</p>
<h2 id="heading-sdn"><strong>SDN:</strong></h2>
<p>SDN stands for Software Defined Network, a network architecture approach that enables network control and management using application software. The network behavior of the entire network and Software Defined Network (SDN) through its devices are programmed in a centrally controlled manner through software using available APIs.</p>
<p>To understand Software Defined Network we need to understand the different levels involved in the network. These levels are:</p>
<ol>
<li><p>Data plane</p>
</li>
<li><p>Control Plane</p>
</li>
</ol>
<h3 id="heading-dataplane"><strong>DataPlane:</strong></h3>
<p>All activities related to data packets sent by the client are related to this section:</p>
<p>1. Sending packets</p>
<p>2. Division and re-collection of data.</p>
<p>3. Repetition of packets for multicasting</p>
<h3 id="heading-control-plane"><strong>Control Plane:</strong></h3>
<p>All activities necessary to perform Data Plane activities except for client data packs. In other words, it is the brain of the network that includes:</p>
<p>1. Create routing tables</p>
<p>2. Setting policies for packet management</p>
<h3 id="heading-sdn-importance"><strong>SDN Importance:</strong></h3>
<p>1. <strong>Better network connectivity:</strong> SDN provides much better network connectivity for sales, service and internal communications. SDN also helps to share data faster.</p>
<p>2. <strong>Better deployment of programs:</strong> The deployment of programs, services, and many new business models can be increased by using Software Defined Networking.</p>
<p>3. <strong>Better security:</strong> SDN provides better visibility across the network. Operators can create separate zones for devices that require different levels of security. SDN networks give operators more freedom.</p>
<p>4. <strong>Better control with high speed:</strong> SDN provides better speed than other types of networks by using a software-based controller.</p>
<p>In short, it can be said that SDN acts as a larger umbrella or a Hub where the rest of the network technologies come and sit under that umbrella and integrate with another platform to reduce traffic and increase the efficiency of data flow, the best result.</p>
<h3 id="heading-where-is-sdn-used"><strong>Where is SDN used?</strong></h3>
<p>Companies use SDN to deploy applications faster while reducing deployment and operational costs. SDN allows network administrators to manage and deliver network services from a single location.</p>
<h3 id="heading-sdn-components"><strong>SDN components:</strong></h3>
<p>The three main components that make up SDN are:</p>
<ol>
<li><p>SDN programs: SDN programs send requests or networks through the SDN Controller using API.</p>
</li>
<li><p>SDN Controller: SDN Controller collects network information from hardware and sends this information to applications.</p>
</li>
<li><p>SDN network devices: SDN network devices help in sending and processing data</p>
</li>
</ol>
<h3 id="heading-sdn-architecture"><strong>SDN architecture:</strong></h3>
<p>In an old network, each switch has its data plane and control plane. The control plane exchanges the topology information between the various switches and thus creates a forward table that decides where the incoming data packet should be forwarded through the data plane.</p>
<p>SDN is an approach through which we separate the Control Plane from the switch and assign it to a centralized unit called the SDN Controller. In this way, the network admin can shape the traffic through the central console without having to access the switches.</p>
<p>The data plane also resides in the switch, and when a packet enters a switch, its forwarding activity is determined based on table entries, which are pre-defined by the controller.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712841577034/020d9a93-2a7b-4f8b-af0c-2e040927b12d.jpeg" alt class="image--center mx-auto" /></p>
<p>A typical SDN architecture consists of three layers.</p>
<p><strong>Application layer:</strong> includes common network applications such as intrusion detection, firewall, load balancer, etc.</p>
<p><strong>Control layer:</strong> includes the SDN Controller, which acts as the brain of the network. It also allows hardware separation for programs written on top of it.</p>
<p><strong>Infrastructure layer:</strong> This layer includes the physical switches that form the data plane and perform the actual movement of data packets.</p>
<p>The layers communicate through sets of connections called (north-bound API between the application and control layer) and (southbound API between the control and infrastructure layer)</p>
<h2 id="heading-sds"><strong>SDS:</strong></h2>
<p>Software-defined Storage or SDS is a storage architecture that separates the storage software from its hardware. Unlike legacy storage systems such as network-attached storage (NAS) or storage area network (SAN), SDS is generally designed to run on any industry-standard or x86 system, eliminating software dependency on proprietary hardware.</p>
<h3 id="heading-advantages-of-sds">Advantages of SDS:</h3>
<p>1. The SDS you choose should not be from the same company that sold you the hardware. You can use any x86 commodity or server to create an SDS-based storage infrastructure. This means you can maximize the capacity of your existing hardware as your storage needs increase.</p>
<p>2. SDS allows you to adjust the capacity and performance completely independently, according to the needs of the organization.</p>
<p>3. SAN storage devices are limited to the number of nodes they can use. SDS, by its very definition, is not limited in this way and is theoretically infinitely scalable.</p>
<h2 id="heading-advantages-of-software-defined-data-center">Advantages of Software-Defined Data Center:</h2>
<p>Based on unique features, Software-Defined Data Center enables organizations to achieve more flexible and faster deployment, management, and business implementation at lower cost.</p>
<ol>
<li><h3 id="heading-business-agility"><strong>Business agility:</strong></h3>
</li>
</ol>
<p>With infrastructure management, automation, and service orchestration functions, SDDC removes the physical dependency of hardware and enables real-time provisioning of resources, which can manage workloads and respond quickly to business demands.</p>
<p>In fact, the time of deployment and provision of resources can be significantly reduced and it does not take much time to provide more storage capacity for applications and modify the physical network.</p>
<ol start="2">
<li><h3 id="heading-increased-scalability"><strong>Increased scalability:</strong></h3>
</li>
</ol>
<p>Cloud-based SDDC allows organizations to scale up or down performance as needed to meet changing demand. Increasing or decreasing IT resources, such as data storage capacity, and network processing power, is very simple. SDDC offers unlimited scalability. No need to worry about freeing up more space to meet growing business needs.</p>
<ol start="3">
<li><h3 id="heading-reduce-costs"><strong>Reduce costs:</strong></h3>
</li>
</ol>
<p>SDDC can help reduce costs. Older data centers require more IT manpower, expensive equipment, time, and maintenance. While in SDDC they can avoid large capital costs. For example, the SDDC pools resources to improve infrastructure utilization and reduce the cost of purchasing new infrastructure. Better utilization also means lower costs for electricity, cooling, etc.</p>
<ol start="4">
<li><h3 id="heading-simple-data-center-management"><strong>Simple data center management:</strong></h3>
</li>
</ol>
<p>SDDC can be managed through a central dashboard, allowing network administrators to monitor data, update systems, and allocate additional storage resources. Compared to legacy data centers, which may require multiple IT tools, applications, and software to manage, SDDC makes data center management much simpler.</p>
]]></content:encoded></item><item><title><![CDATA[Mastering SSH Key-Based Authentication: Tips, Tricks, and Best Practices]]></title><description><![CDATA[SSH key-based authentication provides a sophisticated way to secure remote access to servers and systems, commonly used in Unix-like operating systems. It boasts numerous advantages over traditional password-based methods, including heightened securi...]]></description><link>https://blog.mahdishadi.me/mastering-ssh-key-based-authentication-tips-tricks-and-best-practices</link><guid isPermaLink="true">https://blog.mahdishadi.me/mastering-ssh-key-based-authentication-tips-tricks-and-best-practices</guid><category><![CDATA[ssh]]></category><category><![CDATA[ssh-keys]]></category><category><![CDATA[Linux]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Mahdi Shadi]]></dc:creator><pubDate>Fri, 26 Jan 2024 19:59:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706297785230/eb2646b4-35be-4b96-969a-d81a8b8524d9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>SSH key-based authentication provides a sophisticated way to secure remote access to servers and systems, commonly used in Unix-like operating systems. It boasts numerous advantages over traditional password-based methods, including heightened security, ease of use, and automation capabilities. Let's delve into the essentials of SSH key-based authentication</p>
<h3 id="heading-generating-key-pairs"><strong>Generating Key Pairs:</strong></h3>
<p>This authentication method relies on asymmetric cryptography. Users create a pair of cryptographic keys: a public key and a private key. The public key is placed on the server(s) the user intends to access, while the private key remains safely stored on the user's local system.</p>
<h3 id="heading-placing-public-keys"><strong>Placing Public Keys:</strong></h3>
<p>Users typically append their public key to the <code>~/.ssh/authorized_keys</code> file on the server they wish to access. This file serves as a repository of authorized public keys allowed to log in to the associated account.</p>
<h3 id="heading-safeguarding-private-keys"><strong>Safeguarding Private Keys:</strong></h3>
<p>The private key is a critical piece kept secure on the user's local machine. It's imperative to protect it with robust encryption and permissions. Users can bolster security by using passphrase-protected private keys, adding an extra layer of defense.</p>
<h3 id="heading-authentication-process"><strong>Authentication Process:</strong></h3>
<p>When a user tries to log in to the server, the SSH client sends a challenge encrypted with the user's public key. The server decrypts this challenge using the authorized public key stored in the <code>authorized_keys</code> file. If the decrypted challenge matches the one sent by the client, access is granted.</p>
<h3 id="heading-security-benefits"><strong>Security Benefits:</strong></h3>
<ul>
<li><p><strong>Resistance to Brute Force Attacks</strong>: Since the private key isn't transmitted over the network, SSH key-based authentication is resilient against brute-force attacks.</p>
</li>
<li><p><strong>Elimination of Passwords</strong>: This method eradicates the need for password-based logins, mitigating risks associated with password-related attacks like dictionary attacks or phishing.</p>
</li>
<li><p><strong>Logging and Accountability</strong>: Each user's access is tied to their unique key pair, providing a clear audit trail that enhances accountability and simplifies forensic analysis in case of security incidents.</p>
</li>
</ul>
<h3 id="heading-convenience-and-automation"><strong>Convenience and Automation:</strong></h3>
<ul>
<li><p><strong>Single Sign-On (SSO)</strong>: Once set up, SSH key-based authentication enables seamless access to multiple servers without the hassle of entering passwords repeatedly.</p>
</li>
<li><p><strong>Automated Processes</strong>: SSH keys are commonly used in automated scripts and processes, facilitating secure, passwordless interactions between systems.</p>
</li>
</ul>
<h3 id="heading-key-management"><strong>Key Management:</strong></h3>
<ul>
<li><p><strong>Rotation</strong>: Regularly rotating keys is recommended to reduce the risk of compromised keys.</p>
</li>
<li><p><strong>Revocation</strong>: If a private key is compromised or an employee leaves an organization, the associated public key should be removed from the <code>authorized_keys</code> file to revoke access.</p>
</li>
</ul>
<h3 id="heading-compatibility"><strong>Compatibility:</strong></h3>
<p>SSH key-based authentication enjoys broad support across various SSH implementations and is compatible with SSH clients and servers on different platforms.</p>
<p>SSH key-based authentication is widely utilized across diverse environments to bolster security, simplify access, and streamline operations. Here's a look at some everyday scenarios where SSH key-based authentication plays a crucial role:</p>
<h3 id="heading-1-managing-servers"><strong>1. Managing Servers:</strong></h3>
<p>System administrators rely on SSH key-based authentication to securely access remote servers for tasks like maintenance, configuration, and troubleshooting. This method mitigates the risks associated with password-based logins, ensuring the protection of critical systems.</p>
<h3 id="heading-2-cloud-infrastructure-control"><strong>2. Cloud Infrastructure Control:</strong></h3>
<p>Organizations managing cloud infrastructure, such as AWS, Google Cloud Platform, or Azure, use SSH key-based authentication to securely connect to virtual machines and cloud instances. This practice limits access to authorized personnel, minimizing the chance of unauthorized entry or data breaches.</p>
<h3 id="heading-3-development-environments"><strong>3. Development Environments:</strong></h3>
<p>Developers utilize SSH key-based authentication to access development servers, version control systems (e.g., GitHub or Bitbucket), and other development tools. This enables secure and seamless access to development environments, fostering collaborative software development processes.</p>
<h3 id="heading-4-continuous-integrationcontinuous-deployment-cicd"><strong>4. Continuous Integration/Continuous Deployment (CI/CD):</strong></h3>
<p>In CI/CD pipelines, SSH key-based authentication is employed to authenticate between various stages of the pipeline (e.g., build, test, deploy). Automation tools like Jenkins or GitLab CI/CD use SSH keys to securely access servers, deploy code, and execute deployment scripts without manual intervention.</p>
<h3 id="heading-5-database-management"><strong>5. Database Management:</strong></h3>
<p>Database administrators (DBAs) employ SSH key-based authentication to securely access databases hosted on remote servers. By configuring SSH key-based authentication, DBAs establish secure connections for tasks such as database backups, migrations, and performance tuning.</p>
<h3 id="heading-6-secure-file-transfers"><strong>6. Secure File Transfers:</strong></h3>
<p>Organizations rely on SSH key-based authentication for secure file transfers between systems using protocols like SCP or SFTP. SSH keys ensure secure authentication and encryption of file transfers, preserving the confidentiality and integrity of transmitted data.</p>
<h3 id="heading-7-access-control-systems"><strong>7. Access Control Systems:</strong></h3>
<p>SSH key-based authentication integrates with access control systems to regulate user access to sensitive resources like financial data or proprietary software. Centralized management of SSH keys and access policies helps enforce least privilege access, enhancing overall security.</p>
<h3 id="heading-8-remote-iot-device-management"><strong>8. Remote IoT Device Management:</strong></h3>
<p>In IoT deployments, SSH key-based authentication enables secure access and management of IoT devices from remote locations. Manufacturers and administrators use SSH keys to authenticate and securely communicate with IoT devices for tasks such as monitoring, configuration, and software updates.</p>
<p>These scenarios underscore the versatility and significance of SSH key-based authentication in ensuring secure and authenticated access to a variety of systems and resources across different industries and use cases.</p>
<h2 id="heading-how-can-we-use-it">How can we use it?!</h2>
<p>Suppose I have 2 servers that I named it Ubuntu-SRV1( ip: 192.168.1.43) &amp; Ubuntu-SRV2(ip: 192.168.1.44). I want to connect to Ubuntu-SRV2 from the Ubuntu-SRV1 server with an SSH key.</p>
<p>on Ubuntu-SRV1, I typed this Command:</p>
<pre><code class="lang-bash">root@ubuntu-SRV1:/home/ubuntu<span class="hljs-comment"># ssh-keygen -t rsa</span>
</code></pre>
<p>as you can see from the picture, after this command You can change the path to save .ssh directory files or press enter to save them in the default path.</p>
<p>also, ssh-key requires a passphrase from you to create another security level for you when using this key, but it is optional.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706294075765/5eee47d2-a036-4ae8-98d0-1e537f572547.png" alt class="image--center mx-auto" /></p>
<p>after we create, The access level for the .ssh folder should be 600 like picture.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706294562140/c17da5ca-444e-45bc-b25d-3dd842039727.png" alt class="image--center mx-auto" /></p>
<p>in the .ssh folder, we Have a public key and a Private key.<br />What we did on the first server, we also do for the second server</p>
<pre><code class="lang-bash">root@ubuntusrv2:/home/ubuntu<span class="hljs-comment"># ssh-key</span>
</code></pre>
<p>now, I should copy the ubuntu-SRV1 public key to ubuntu-SRV2</p>
<p>To do this, use this Command:</p>
<pre><code class="lang-bash">root@ubuntu-SRV1:/home/ubuntu<span class="hljs-comment"># ssh-copy-id ubuntu@192.168.1.44</span>
</code></pre>
<p>after this Command, we copied ubuntu-SRV1 public key into ubuntu-SRV2 server.</p>
<p>Now, we can use SSH from the ubuntu-SRV1 server to access the ubuntu-SRV2 server without using the Password.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706296898908/e8e55a37-4f24-4532-b953-aefa10161ee9.png" alt class="image--center mx-auto" /></p>
<p>In summary, SSH key-based authentication offers a robust and secure means of remote access, particularly suitable for environments prioritizing security, convenience, and automation. Adhering to best practices in key management and security is vital for maintaining its efficacy.</p>
]]></content:encoded></item></channel></rss>