<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Under the hood - All Subjects Matter</title>
    <link>https://www.allsubjectsmatter.nl/tag/under-the-hood/</link>
    <description>How Remco Loup, CEO/CTO of 333travel, looks at problems and solves them: with data, with teams, and with whatever technology fits.</description>
    <language>en</language>
    <image><url>https://www.allsubjectsmatter.nl/images/icon.png</url><title>All Subjects Matter</title><link>https://www.allsubjectsmatter.nl/</link></image>
    <lastBuildDate>Fri, 29 Nov 2024 16:45:40 +0100</lastBuildDate>
    <atom:link href="https://www.allsubjectsmatter.nl/tag/under-the-hood/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Stay Connected with a dynamic DNS updater for TransIP</title>
      <link>https://www.allsubjectsmatter.nl/stay-connected-with-a-dynamic-dns-updater-for-transip/</link>
      <guid isPermaLink="false">6749dfb5c2a33e00011cc689</guid>
      <pubDate>Fri, 29 Nov 2024 16:45:40 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Easily access your home PC despite a dynamic IP! This script automatically updates your TransIP DNS records with your current IP, ensuring seamless connectivity. Secure, customizable, and perfect for remote work or hosting services. Stay connected effortlessly!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/stay-connected-with-a-dynamic-dns-updater-for-transip/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>In a world with DHCP address, being able to access your home PC or server remotely is a Pita. So, if you’re are &ldquo;blessed&quot;with an ISP that assigns a <strong>dynamic public IP address</strong>, you face a significant challenge: your IP changes periodically, breaking your ability to connect to your home device using a static domain.</p>
<p>This is where this free <strong>Dynamic DNS (DDNS) updater script</strong> for TransIP comes to the rescue. By seamlessly updating your DNS records whenever your external IP changes, this solution ensures your domain (e.g., <code>vpn.example.com</code>) always points to the correct IP. Here’s why this script is a must for anyone leveraging TransIP’s DNS services.</p>
<h2 id="key-features-of-the-script">Key Features of the Script</h2>
<ol>
<li><strong>Automated DNS Updates:</strong>
<ul>
<li>Automatically checks your current external IP and compares it to the DNS A-record associated with your domain.</li>
<li>Creates or updates the A-record to match your latest external IP.</li>
</ul>
</li>
<li><strong>Dynamic DNS for Dynamic IPs:</strong>
<ul>
<li>Perfect for environments where your public IP changes frequently, such as home internet connections with dynamic DHCP assignments.</li>
</ul>
</li>
<li><strong>Secure Authentication:</strong>
<ul>
<li>Leverages TransIP’s API with a private key for secure communication.</li>
<li>Generates short-lived tokens for added security (valid for only 10 minutes per session).</li>
</ul>
</li>
<li><strong>Customizable Settings:</strong>
<ul>
<li>Easily configure domain, subdomain, TTL (time-to-live), and logging options.</li>
<li>Adjust the script to match your specific requirements.</li>
</ul>
</li>
<li><strong>Logging and Error Handling:</strong>
<ul>
<li>Comprehensive logging ensures you can track every step of the update process.</li>
<li>Built-in error handling for smoother execution and debugging.</li>
</ul>
</li>
</ol>
<h2 id="how-it-works">How It Works</h2>
<ol>
<li><strong>Initial Setup:</strong>
<ul>
<li>Set up a TransIP account and generate a private key for API access at <a href="https://www.transip.nl/cp/account/api/" data-external>https://www.transip.nl/cp/account/api/</a></li>
<li>Configure the script with your domain name, subdomain, and private key path.</li>
</ul>
</li>
<li><strong>Fetching External IP:</strong>
<ul>
<li>The script retrieves your current external IP using a public API like <code>ipinfo.io</code>.</li>
</ul>
</li>
<li><strong>Token Generation:</strong>
<ul>
<li>Generates a short-lived access token using TransIP’s authentication endpoint.</li>
</ul>
</li>
<li><strong>DNS Record Validation:</strong>
<ul>
<li>Checks if the current DNS A-record matches your external IP.</li>
<li>If it differs, the script updates the record; if it’s missing, it creates a new one.</li>
</ul>
</li>
<li><strong>DNS Update:</strong>
<ul>
<li>Uses TransIP’s secure API to update or create the DNS A-record with your current assigned external IP.</li>
</ul>
</li>
<li><strong>Logging and Notifications:</strong>
<ul>
<li>Logs each step and provides clear messages in case of issues.</li>
</ul>
</li>
</ol>
<h2 id="prerequisites">Prerequisites</h2>
<p>Before using this script, ensure you have:</p>
<ol>
<li>A TransIP account with API access enabled.</li>
<li>A registered domain in your TransIP account.</li>
<li>A static private key for secure API authentication.</li>
<li>Dependencies installed on your system:
<ul>
<li><code>curl</code>, <code>openssl</code>, and <code>jq</code>.</li>
</ul>
</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt update
sudo apt install -y curl openssl jq</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><h2 id="why-use-this-solution">Why Use This Solution?</h2>
<p>Unlike payed third-party DDNS providers, this script gives you free control over your DNS management, ensuring privacy and avoiding vendor lock-in. It’s tailored for you.</p>
<pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

# Configuration settings
LOGIN_NAME=&amp;#34;your-login-name&amp;#34;  # Fill in your TransIP login name
PRIVATE_KEY_PATH=&amp;#34;/path/transip_private_key.pem&amp;#34;  # Path to your private key file
AUTH_URL=&amp;#34;https://api.transip.nl/v6/auth&amp;#34;
TOKEN_FILE=&amp;#34;/var/tmp/transip_token.json&amp;#34;  # File to store token and expiration date
DOMAIN=&amp;#34;example.com&amp;#34;  # Your main or test domain
SUBDOMAIN=&amp;#34;vpn&amp;#34;  # The subdomain you want to update (e.g., test.example.com)
RECORD_TYPE=&amp;#34;A&amp;#34;
TTL=60  # Time-to-live for DNS records
LOG_FILE=&amp;#34;/var/log/transip_dyndns.log&amp;#34;

# Function to update a log file
log_message() {
    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34; &amp;gt; $LOG_FILE
}

# Function to log an error and stop the script
handle_error() {
    log_message &amp;#34;ERROR: $1&amp;#34;
    echo &amp;#34;ERROR: $1&amp;#34; &amp;gt;&amp;amp;2
    exit 1
}

# Function to obtain a new access token with a short validity period (10 minutes)
generate_new_token() {
    log_message &amp;#34;Generating a new token with a validity period of 10 minutes.&amp;#34;
    NONCE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
    TIMESTAMP=$(date &#43;%s)
    LABEL=&amp;#34;Set DNS to external DHCP $TIMESTAMP&amp;#34;
    REQUEST_BODY=$(printf &amp;#39;{&amp;#34;login&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;nonce&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;read_only&amp;#34;:false,&amp;#34;expiration_time&amp;#34;:&amp;#34;10 minutes&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;global_key&amp;#34;:true}&amp;#39; &amp;#34;$LOGIN_NAME&amp;#34; &amp;#34;$NONCE&amp;#34; &amp;#34;$LABEL&amp;#34;)
    log_message &amp;#34;Request Body: $REQUEST_BODY&amp;#34;
    SIGNATURE=$(echo -n &amp;#34;$REQUEST_BODY&amp;#34; | openssl dgst -sha512 -sign &amp;#34;$PRIVATE_KEY_PATH&amp;#34; | base64 | tr -d &amp;#39;\n&amp;#39;)
    log_message &amp;#34;Generated signature: $SIGNATURE&amp;#34;
    RESPONSE=$(curl -s -X POST &amp;#34;$AUTH_URL&amp;#34; \
        -H &amp;#34;Signature: $SIGNATURE&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$REQUEST_BODY&amp;#34;) || handle_error &amp;#34;Failed to connect to TransIP API&amp;#34;
    log_message &amp;#34;API-response received: $RESPONSE&amp;#34;
    ACCESS_TOKEN=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r &amp;#39;.token // empty&amp;#39;)
    if [ -z &amp;#34;$ACCESS_TOKEN&amp;#34; ]; then
        handle_error &amp;#34;The received access token is empty or invalid. Check the API-response: $RESPONSE&amp;#34;
    fi
    log_message &amp;#34;Received token: $ACCESS_TOKEN&amp;#34;
}

# Function to get the current DNS A-record from TransIP
get_dns_record() {
    # Get API response and store status code
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Check if response contains JSON and no errors
    if echo &amp;#34;$RESPONSE&amp;#34; | jq empty 2&amp;gt;/dev/null; then
        RECORD_CONTENT=$(echo $RESPONSE | jq -r &amp;#39;.dnsEntries[] | select(.name==&amp;#34;&amp;#39;$SUBDOMAIN&amp;#39;&amp;#34; and .type==&amp;#34;&amp;#39;$RECORD_TYPE&amp;#39;&amp;#34;) | .content&amp;#39;)
    else
        handle_error &amp;#34;API response does not contain valid JSON or contains an error: $RESPONSE&amp;#34;
    fi

    # Check if we found a valid record
    if [ -z &amp;#34;$RECORD_CONTENT&amp;#34; ]; then
        log_message &amp;#34;No valid DNS record found for $SUBDOMAIN.$DOMAIN. Record may need to be created.&amp;#34;
        return 1
    fi

    echo &amp;#34;$RECORD_CONTENT&amp;#34;
    return 0
}

# Function to update or create DNS record
update_or_create_dns_record() {
    # Check if record exists
    CURRENT_DNS_IP=$(get_dns_record)

    if [ -z &amp;#34;$CURRENT_DNS_IP&amp;#34; ]; then
        log_message &amp;#34;No DNS A-record found for $SUBDOMAIN.$DOMAIN. Creating record.&amp;#34;
        # Create request body for new record (TTL set to 60 seconds)
        DNS_ENTRY=$(jq -n \
        --arg name &amp;#34;$SUBDOMAIN&amp;#34; \
        --arg expire &amp;#34;60&amp;#34; \
        --arg type &amp;#34;$RECORD_TYPE&amp;#34; \
        --arg content &amp;#34;$CURRENT_IP&amp;#34; \
        &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

        log_message &amp;#34;DNS entry (create): $DNS_ENTRY&amp;#34;

        # Execute POST request to create new DNS entry with better status code handling
        RESPONSE=$(curl -s -w &amp;#34;%{http_code}&amp;#34; -X POST \
            -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
            -H &amp;#34;Content-Type: application/json&amp;#34; \
            -d &amp;#34;$DNS_ENTRY&amp;#34; \
            &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

        HTTP_STATUS_UPDATE=$(echo &amp;#34;$RESPONSE&amp;#34; | tail -n1)
        RESPONSE_BODY=$(echo &amp;#34;$RESPONSE&amp;#34; | head -n -1)

        log_message &amp;#34;API response for creating DNS entry: $RESPONSE_BODY&amp;#34;
        log_message &amp;#34;HTTP status code received for creation: $HTTP_STATUS_UPDATE&amp;#34;

        if [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 201 ]; then
            handle_error &amp;#34;API call for creating DNS entry failed with status code $HTTP_STATUS_UPDATE.&amp;#34;
        fi

        log_message &amp;#34;New DNS A-record successfully created.&amp;#34;
    else
        log_message &amp;#34;Updating DNS A-record for $SUBDOMAIN.$DOMAIN to IP $CURRENT_IP&amp;#34;
        # Create request body for updating the record
        DNS_ENTRY=$(jq -n \
        --arg name &amp;#34;$SUBDOMAIN&amp;#34; \
        --arg expire &amp;#34;$TTL&amp;#34; \
        --arg type &amp;#34;$RECORD_TYPE&amp;#34; \
        --arg content &amp;#34;$CURRENT_IP&amp;#34; \
        &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

        log_message &amp;#34;DNS entry (update): $DNS_ENTRY&amp;#34;

        # Execute PATCH request to update the existing DNS record
        RESPONSE=$(curl -s -w &amp;#34;%{http_code}&amp;#34; -X PATCH \
            -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
            -H &amp;#34;Content-Type: application/json&amp;#34; \
            -d &amp;#34;$DNS_ENTRY&amp;#34; \
            &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

        HTTP_STATUS_UPDATE=$(echo &amp;#34;$RESPONSE&amp;#34; | tail -n1)
        RESPONSE_UPDATE_BODY=$(echo &amp;#34;$RESPONSE&amp;#34; | head -n -1)

        log_message &amp;#34;API update response received: $RESPONSE_UPDATE_BODY&amp;#34;
        log_message &amp;#34;HTTP status code received for update: $HTTP_STATUS_UPDATE&amp;#34;

        # Accept both 200 and 204 as successful updates
        if [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 200 ] &amp;amp;&amp;amp; [ &amp;#34;$HTTP_STATUS_UPDATE&amp;#34; -ne 204 ]; then
            handle_error &amp;#34;API call for DNS update failed with status code $HTTP_STATUS_UPDATE.&amp;#34;
        else
            log_message &amp;#34;DNS A-record successfully updated to $CURRENT_IP&amp;#34;
        fi
    fi
}

# Get the current external IP
CURRENT_IP=$(curl -s http://ipinfo.io/ip) || handle_error &amp;#34;Failed to get external IP&amp;#34;

# Log the start
log_message &amp;#34;---- Start new run ----&amp;#34;
log_message &amp;#34;Current external IP: $CURRENT_IP&amp;#34;

# Always generate a short lived new token.
generate_new_token


# Try to update or create the DNS record
update_or_create_dns_record

log_message &amp;#34;---- End of run ----&amp;#34;</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre>]]></content:encoded>
    </item>
    <item>
      <title>Automating Let&#39;s Encrypt with Certbot and TransIP using Docker</title>
      <link>https://www.allsubjectsmatter.nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/</link>
      <guid isPermaLink="false">6749c93bc2a33e00011cc621</guid>
      <pubDate>Thu, 28 Nov 2024 22:03:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Want to auto update/manage your SSL certificates inside your Docker with Certbot and hosting your domain with TransIP? Then look no further!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Managing SSL certificates for domains hosted on TransIP can be cumbersome without automation. TransIP luckily offers a robust API for DNS management, but to integrate it seamlessly with Certbot for <code>dns-01</code> validation, a custom solution is necessary. Since I couldn’t find a ready-made script for this purpose, I created one that securely handles authentication, DNS updates, and certificate issuance.</p>
<p>You can go to the full API documentation here:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://api.transip.nl/rest/docs.html" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">TransIP API</span><span class="bookmark__description"></span><span class="bookmark__meta"><img src="https://www.allsubjectsmatter.nl/automating-lets-encrypt-with-certbot-and-transip-using-docker-2/bookmarks/01-icon.ico" sizes="25px" alt="" class="bookmark__icon" loading="lazy" decoding="async"></span></span></a></figure>

<p>This guide explains the full setup using Docker, including how to generate the API key pair in TransIP’s web interface and use it for automated certificate management.</p>
<h2 id="how-transip-authentication-works">How TransIP authentication works</h2>
<p>TransIP uses an RSA key pair for API authentication. Here’s how to set it up:</p>
<ol>
<li><strong>Generate the Key Pair in TransIP</strong>:
<ul>
<li>Log in to your <a href="https://www.transip.nl/" data-external>TransIP control panel</a>.</li>
<li>Go to <strong>API Settings</strong> under your account and enable API access.</li>
<li>Generate a new API key pair.</li>
<li><strong>Download/Save the private key</strong> (<code>transip_private_key.pem</code>) securely and create a secret from it to use in your Docker setup.</li>
</ul>
</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">docker secret create transip_private_key transip_private_key.pem</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol>
<li><strong>Keep the Private Key Secure</strong>:
<ul>
<li>Store the private key somewhere safe after creating a secret from it.</li>
<li>This private key is needed to sign requests to TransIP’s API.</li>
</ul>
</li>
<li><strong>How It Works</strong>:
<ul>
<li>The script uses the private key to sign an authentication request.</li>
<li>TransIP’s API responds with a short-lived token (10 minutes by default), which is then used to manage the DNS records.</li>
<li>It will create a new token for convenience on every run. This made it more resilient then checking for an active token and using that which made the whole script less resilient and cumbersome.</li>
</ul>
</li>
</ol>
<h2 id="what-this-setup-is-going-to-do">What this setup is going to do</h2>
<ol>
<li><strong>Automates SSL Certificates</strong>:
<ul>
<li>Automates issuance and renewal of wildcard and domain-specific certificates.</li>
</ul>
</li>
<li><strong>Handles DNS Challenges</strong>:
<ul>
<li>Manages <code>dns-01</code> challenges by creating and deleting DNS TXT records via the TransIP API.</li>
</ul>
</li>
<li><strong>Seamless Integration</strong>:
<ul>
<li>Encapsulates everything in a Docker container for portability and simplicity.</li>
<li>You can remove or add domains in this script on the -d rule part. I put the examples in for a better explanation.</li>
</ul>
</li>
</ol>
<p>We will pipe a add or clean command to the bash script to take the necessary actions as defined in the script. So we use the &ndash;manual-auth-hook and the manual-cleanup-hook here. This makes sure that a fail in the add section still cleans up after itself for example.</p>
<p>You can also add a &ndash;force to this script. but your will run out of attempts to Letsencrypt quickly since they only allow you to do a few calls every 24 hours. But this will make sure it refreshes the certificates always when you need to test or verify something.</p>
<pre class="line-numbers language-yaml"><code class="language-yaml">version: &amp;#34;3.8&amp;#34;

services:
  certbot:
    image: certbot/certbot:latest
    container_name: certbot-transip
    secrets:
      - transip_private_key  # Reference the secret you created earlier
    # adjust these volumes to point to the right .pem and script. right now this dockerfile expects the files to be in the same folder as the docker-certbot-transip.yml
    volumes:
      - &amp;#34;./certbot-transip.sh:/usr/local/bin/certbot-transip.sh:ro&amp;#34;
      - &amp;#34;./letsencrypt:/etc/letsencrypt&amp;#34;
    environment:
    #These Variables are going to be piped in our bash script.
      LOGIN_NAME: &amp;#34;your-transip-login&amp;#34;        # Replace with your TransIP login name
      PRIVATE_KEY_PATH: &amp;#34;/etc/transip_private_key.pem&amp;#34;
      DOMAIN: &amp;#34;example.com&amp;#34;                  # Replace with your domain
      TTL: &amp;#34;60&amp;#34;                              # Optional: Time-to-live for DNS challenge
      CERTBOT_EMAIL: &amp;#34;you@example.com&amp;#34;       # Replace with your valid email as requested by letsencrypt.
    entrypoint:
      - sh
      - -c
      - |
        apk update &amp;amp;&amp;amp; apk add --no-cache bash curl jq bind-tools &amp;amp;&amp;amp; \
        chmod &#43;x /usr/local/bin/certbot-transip.sh &amp;amp;&amp;amp; \
        certbot certonly --manual --preferred-challenges dns \
          --manual-auth-hook &amp;#34;/usr/local/bin/certbot-transip.sh add&amp;#34; \
          --manual-cleanup-hook &amp;#34;/usr/local/bin/certbot-transip.sh clean&amp;#34; \
          --cert-name &amp;#34;$DOMAIN&amp;#34; \
          -d &amp;#34;*.$DOMAIN&amp;#34; \
          -d &amp;#34;example2.$DOMAIN&amp;#34; \
          -d &amp;#34;example3.$DOMAIN&amp;#34; \
          --expand \
          --non-interactive --agree-tos \
          --email &amp;#34;$CERTBOT_EMAIL&amp;#34;

secrets:
  transip_private_key:
    external: true  # Secret must already exist in Docker</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="the-bash-script">The Bash script</h2>
<p>This is the &ldquo;advanced&rdquo; part that will do its magic and make everything work with TransIP. I tried to make it a bit more resilient then standard also. So this should hopefully help in creating a resilient and good working environment.</p>
<p>You can also use this TransIP script to run it locally creating certificates or in a Kubectl or Helm chart.</p>
<pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

# Configuration settings
LOGIN_NAME=${LOGIN_NAME}
TTL=${TTL:-60}  # Default value 60 if TTL is not set
PRIVATE_KEY_PATH=${PRIVATE_KEY_PATH}
DOMAIN=${DOMAIN}

AUTH_URL=&amp;#34;https://api.transip.nl/v6/auth&amp;#34;
LOG_FILE=&amp;#34;/var/log/transip_dyndns.log&amp;#34;

# Function to update a log file
#log_message() {
#    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34; &amp;gt;&amp;gt; $LOG_FILE
#}

# Function to log messages to stdout
log_message() {
    echo &amp;#34;$(date &amp;#39;&#43;%Y-%m-%d %H:%M:%S&amp;#39;) - $1&amp;#34;
}

# Function to log an error and stop the script
handle_error() {
    log_message &amp;#34;ERROR: $1&amp;#34;
    echo &amp;#34;ERROR: $1&amp;#34; &amp;gt;&amp;amp;2
    exit 1
}

# Function to obtain a new access token with a short validity period (10 minutes)
generate_new_token() {
    log_message &amp;#34;Generating a new token with a validity period of 10 minutes.&amp;#34;

    NONCE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
    TIMESTAMP=$(date &#43;%s)
    LABEL=&amp;#34;Certbot DNS update $TIMESTAMP&amp;#34;

    REQUEST_BODY=$(printf &amp;#39;{&amp;#34;login&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;nonce&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;read_only&amp;#34;:false,&amp;#34;expiration_time&amp;#34;:&amp;#34;10 minutes&amp;#34;,&amp;#34;label&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;global_key&amp;#34;:true}&amp;#39; &amp;#34;$LOGIN_NAME&amp;#34; &amp;#34;$NONCE&amp;#34; &amp;#34;$LABEL&amp;#34;)

    log_message &amp;#34;Request Body: $REQUEST_BODY&amp;#34;

    SIGNATURE=$(echo -n &amp;#34;$REQUEST_BODY&amp;#34; | openssl dgst -sha512 -sign &amp;#34;$PRIVATE_KEY_PATH&amp;#34; | base64 | tr -d &amp;#39;\n&amp;#39;)

    log_message &amp;#34;Generated signature: $SIGNATURE&amp;#34;

    RESPONSE=$(curl -s -X POST &amp;#34;$AUTH_URL&amp;#34; \
        -H &amp;#34;Signature: $SIGNATURE&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$REQUEST_BODY&amp;#34;) || handle_error &amp;#34;Failed to connect to TransIP API&amp;#34;

    log_message &amp;#34;API-response received: $RESPONSE&amp;#34;

    ACCESS_TOKEN=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r &amp;#39;.token // empty&amp;#39;)
    if [ -z &amp;#34;$ACCESS_TOKEN&amp;#34; ]; then
        handle_error &amp;#34;The received access token is empty or invalid. Check the API-response: $RESPONSE&amp;#34;
    fi

    log_message &amp;#34;Received token: $ACCESS_TOKEN&amp;#34;
}

# Function to add or update a TXT record
add_or_update_txt_record() {
    local full_domain=&amp;#34;$1&amp;#34;
    local value=&amp;#34;$2&amp;#34;

    # Extract the subdomain part from the full domain
    local name=&amp;#34;_acme-challenge.${full_domain%%.*}&amp;#34;

    if [ -z &amp;#34;$name&amp;#34; ] || [ -z &amp;#34;$value&amp;#34; ]; then
        log_message &amp;#34;ERROR: Name or value for TXT record is missing.&amp;#34;
        exit 1
    fi

    # Check if the DNS record already exists
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Search for all existing records with the same name and type TXT
    MATCHING_RECORDS=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r --arg name &amp;#34;$name&amp;#34; &amp;#39;[.dnsEntries[] | select(.name==$name and .type==&amp;#34;TXT&amp;#34;)]&amp;#39;)

    # Check if there are multiple or no matching records
    MATCHING_COUNT=$(echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq &amp;#39;length&amp;#39;)

    # Ensure MATCHING_COUNT is always an integer
    if ! [[ &amp;#34;$MATCHING_COUNT&amp;#34; =~ ^[0-9]&#43;$ ]]; then
        MATCHING_COUNT=0
    fi

    if [ &amp;#34;$MATCHING_COUNT&amp;#34; -eq 0 ]; then
        log_message &amp;#34;No existing TXT record found for $name. Creating a new record.&amp;#34;
    elif [ &amp;#34;$MATCHING_COUNT&amp;#34; -ge 1 ]; then
        log_message &amp;#34;Multiple or one existing TXT record found for $name. Removing all existing records.&amp;#34;

        # Remove all existing records using the full details of each record
        echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq -c &amp;#39;.[]&amp;#39; | while read -r record; do
            existing_name=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.name&amp;#39;)
            existing_expire=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.expire&amp;#39;)
            existing_content=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.content&amp;#39;)

            # Log the details of the record to be removed
            log_message &amp;#34;Remove record: name=$existing_name, expire=$existing_expire, content=$existing_content&amp;#34;

            # Make the DELETE call using the full DNS-entry details
            DELETE_ENTRY=$(jq -n \
            --arg name &amp;#34;$existing_name&amp;#34; \
            --arg expire &amp;#34;$existing_expire&amp;#34; \
            --arg type &amp;#34;TXT&amp;#34; \
            --arg content &amp;#34;$existing_content&amp;#34; \
            &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

            DELETE_RESPONSE=$(curl -s -X DELETE \
                -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
                -H &amp;#34;Content-Type: application/json&amp;#34; \
                -d &amp;#34;$DELETE_ENTRY&amp;#34; \
                &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

            # Log the response of the DELETE call
            log_message &amp;#34;Response after removing $existing_name: $DELETE_RESPONSE&amp;#34;

            # Check if the removal was successful
            if [[ &amp;#34;$DELETE_RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
                log_message &amp;#34;ERROR: The record $existing_name could not be removed.&amp;#34;
            else
                log_message &amp;#34;Record $existing_name successfully removed.&amp;#34;
            fi
        done
    fi

    # Create a new record
    DNS_ENTRY=$(jq -n \
    --arg name &amp;#34;$name&amp;#34; \
    --arg expire &amp;#34;$TTL&amp;#34; \
    --arg type &amp;#34;TXT&amp;#34; \
    --arg content &amp;#34;$value&amp;#34; \
    &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

    RESPONSE=$(curl -s -X POST \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$DNS_ENTRY&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    log_message &amp;#34;API-response for TXT record creation: $RESPONSE&amp;#34;

    if [[ &amp;#34;$RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
        handle_error &amp;#34;The TXT record could not be created. Check the API-response: $RESPONSE&amp;#34;
    else
        log_message &amp;#34;TXT record successfully added for $name with value $value&amp;#34;
    fi
}

# Function to update an existing TXT record
update_txt_record() {
    local name=&amp;#34;$1&amp;#34;
    local value=&amp;#34;$2&amp;#34;

    DNS_ENTRY=$(jq -n \
    --arg name &amp;#34;$name&amp;#34; \
    --arg expire &amp;#34;$TTL&amp;#34; \
    --arg type &amp;#34;TXT&amp;#34; \
    --arg content &amp;#34;$value&amp;#34; \
    &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

    RESPONSE=$(curl -s -X PATCH \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        -H &amp;#34;Content-Type: application/json&amp;#34; \
        -d &amp;#34;$DNS_ENTRY&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    log_message &amp;#34;API-response for TXT record update: $RESPONSE&amp;#34;

    if [[ &amp;#34;$RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
        handle_error &amp;#34;The TXT record could not be updated. Check the API-response: $RESPONSE&amp;#34;
    else
        log_message &amp;#34;TXT record successfully updated for $name with value $value&amp;#34;
    fi
}

# Function to remove a TXT record for `dns-01` validation
remove_txt_record() {
    local full_domain=&amp;#34;$1&amp;#34;

    # Extract the subdomain part from the full domain
    local name=&amp;#34;_acme-challenge.${full_domain%%.*}&amp;#34;

    # Check if the DNS record exists
    RESPONSE=$(curl -s -X GET \
        -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
        &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

    # Search for all records with the same name and type TXT
    MATCHING_RECORDS=$(echo &amp;#34;$RESPONSE&amp;#34; | jq -r --arg name &amp;#34;$name&amp;#34; &amp;#39;[.dnsEntries[] | select(.name==$name and .type==&amp;#34;TXT&amp;#34;)]&amp;#39;)

    # Check if there are existing records that need to be removed
    MATCHING_COUNT=$(echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq &amp;#39;length&amp;#39;)

    if [ &amp;#34;$MATCHING_COUNT&amp;#34; -eq 0 ]; then
        log_message &amp;#34;No existing TXT record found for $name to remove.&amp;#34;
    else
        log_message &amp;#34;Removing all existing TXT records for $name.&amp;#34;

        # Loop through all found records and remove them
        echo &amp;#34;$MATCHING_RECORDS&amp;#34; | jq -c &amp;#39;.[]&amp;#39; | while read -r record; do
            existing_name=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.name&amp;#39;)
            existing_expire=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.expire&amp;#39;)
            existing_content=$(echo &amp;#34;$record&amp;#34; | jq -r &amp;#39;.content&amp;#39;)

            # Make the DELETE call using the full details
            DELETE_ENTRY=$(jq -n \
            --arg name &amp;#34;$existing_name&amp;#34; \
            --arg expire &amp;#34;$existing_expire&amp;#34; \
            --arg type &amp;#34;TXT&amp;#34; \
            --arg content &amp;#34;$existing_content&amp;#34; \
            &amp;#39;{&amp;#34;dnsEntry&amp;#34;: {&amp;#34;name&amp;#34;: $name, &amp;#34;expire&amp;#34;: ($expire | tonumber), &amp;#34;type&amp;#34;: $type, &amp;#34;content&amp;#34;: $content}}&amp;#39;)

            DELETE_RESPONSE=$(curl -s -X DELETE \
                -H &amp;#34;Authorization: Bearer $ACCESS_TOKEN&amp;#34; \
                -H &amp;#34;Content-Type: application/json&amp;#34; \
                -d &amp;#34;$DELETE_ENTRY&amp;#34; \
                &amp;#34;https://api.transip.nl/v6/domains/$DOMAIN/dns&amp;#34;)

            # Check if the removal was successful
            if [[ &amp;#34;$DELETE_RESPONSE&amp;#34; == *&amp;#34;error&amp;#34;* ]]; then
                log_message &amp;#34;ERROR: The TXT record $existing_name could not be removed.&amp;#34;
            else
                log_message &amp;#34;TXT record $existing_name successfully removed.&amp;#34;
            fi
        done
    fi
}

# Always generate a new token for each run
generate_new_token

# Certbot passes these values when adding and removing the record
if [ &amp;#34;$1&amp;#34; == &amp;#34;add&amp;#34; ]; then
    add_or_update_txt_record &amp;#34;$CERTBOT_DOMAIN&amp;#34; &amp;#34;$CERTBOT_VALIDATION&amp;#34;
    sleep 25  # Wait for DNS update to propagate
elif [ &amp;#34;$1&amp;#34; == &amp;#34;clean&amp;#34; ]; then
    remove_txt_record &amp;#34;$CERTBOT_DOMAIN&amp;#34;
fi</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="steps-to-run">Steps to run</h2>
<ol>
<li><strong>Download the Private Key</strong>: Save the private key file (<code>transip_private_key.pem</code>) from TransIP’s API settings somewhere on your device you are going to run this from.</li>
<li><strong>Set Up Docker Compose</strong>: Place the <code>docker-compose.yml</code> and <code>certbot-transip.sh</code> files in the same directory. (or edit the script)</li>
<li><strong>Run the Container</strong>: Start the container (cd to the path where your yml is)</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">docker-compose up --build</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Monitor the Log output: The log output of the docker container will show the progress of DNS challenges and certificate issuance.</p>
<p>Thats all there is to it!</p>
<p>I will also post a version with a working helm chart soon. This was a little bigger culprit to make sure it all works correctly and updates all certificates through different namespaces where i needed the certificates to be available on different domains.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deprecated: Supercharge Your headless (Raspberry Pi) VPN with ProtonVPN and WireGuard</title>
      <link>https://www.allsubjectsmatter.nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/</link>
      <guid isPermaLink="false">66f3e524e2acb1000109843a</guid>
      <pubDate>Wed, 25 Sep 2024 22:24:00 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Deprecated: Effortlessly manage ProtonVPN on your headless device like a Raspberry Pi with this Python script. Automatically connect to the fastest server using WireGuard for optimal speed and security. This is to mitigate error 422 using the protonvpn-cli client which is broken as of now.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>The API: <a href="https://api.protonvpn.ch/vpn/logicals" data-external>https://api.protonvpn.ch/vpn/logicals</a> has been formally shut down according to Proton Support. The only thing it returns now is:</p>
<pre class="line-numbers language-json"><code class="language-json">{&amp;#34;Code&amp;#34;:0,&amp;#34;Error&amp;#34;:&amp;#34;Invalid access token&amp;#34;,&amp;#34;Details&amp;#34;:{}}</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>So this post is sadly obsolete now. Somebody started a petition to get this feature back.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://protonmail.uservoice.com/forums/932836-proton-vpn/suggestions/49774472-re-enable-access-to-api" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Re-enable access to API</span><span class="bookmark__description">Until recently, users could access the API (https://api.protonvpn.ch/vpn/logicals) to filter servers based on criteria such as country and server load, to make sure they connect to a server that isn&#39;t overloaded. This ability was removed. 27 votes, 4 comments.</span><span class="bookmark__meta"><span>Proton VPN feedback</span></span></span></a></figure>

<h2 id="the-deprecated-guide-which-no-longer-functions-as-of-062025-for-historical-reasons">The deprecated guide which no longer functions as of 06/2025 For historical reasons.</h2>
<p>When ProtonVPN discontinued its command-line interface (CLI) tool, users of headless systems like Raspberry Pi were left without an easy method to manage VPN connections. Without a graphical user interface, manually switching VPN servers has become a cumbersome task. Also selecting the fastest VPN in a pool of servers has become impossible.</p>
<p>To solve this problem, I developed a Python script that automatically selects the fastest ProtonVPN server based on latency and load, then updates your WireGuard configuration for you. This guide will walk you through setting up WireGuard for ProtonVPN, installing the script, and automating your VPN connection to ensure your Raspberry Pi is always connected to the best server.</p>
<h3 id="why-do-i-need-this-protonss-script">Why do i need this Protonss script?</h3>
<p>This Python script automates the process of connecting your Raspberry Pi to the fastest ProtonVPN server available, using <strong>WireGuard</strong> as the VPN protocol. It checks the latency and load of multiple VPN servers from ProtonVPN&rsquo;s API, selects the one with the best performance, updates your WireGuard configuration with the new server information, and reconnects your VPN only if a significantly faster server is found (nobody actually likes reconnects). This script also logs the entire process for easier troubleshooting.</p>
<ol>
<li><strong>No more manual VPN switching</strong>: Without ProtonVPN&rsquo;s CLI, switching to the fastest server would require manual work. This script automates the process by regularly checking the available servers and updating the connection based on real-time performance.</li>
<li><strong>Optimizes connection speed</strong>: The script ensures you’re always connected to the VPN server with the lowest latency and load. This is especially useful for maintaining high-speed connections in countries with fast or low-traffic ProtonVPN servers.</li>
<li><strong>Reduces downtime</strong>: By automating the reconnection process, the script helps minimize potential downtimes caused by poor server performance or slow connections.</li>
<li><strong>Tailored for Raspberry Pi</strong>: If you&rsquo;re running a headless Raspberry Pi (without a monitor), this script is perfect for automating VPN tasks without requiring constant oversight.</li>
<li><strong>Security &amp; Privacy</strong>: By ensuring your Raspberry Pi always connects to a VPN, it helps protect your data and online activity, especially if you&rsquo;re using the Pi as a torrenting or networking device.</li>
</ol>
<p>In short, this script automates finding the optimal ProtonVPN server, providing faster speeds, more reliability, and convenience for headless systems like a Raspberry Pi.</p>
<h2 id="lets-dive-in">Let&rsquo;s dive in!</h2>
<p>First, you need to install WireGuard, which is a fast, lightweight VPN protocol which we will use to connect to our ProtonVPN server.</p>
<pre class="line-numbers language-bash"><code class="language-bash"># First, update your package list and install WireGuard:
sudo apt update
sudo apt install wireguard curl resolvconf</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p><strong>Generate a WireGuard configuration from ProtonVPN</strong></p>
<ul>
<li>Log in to your ProtonVPN account on their website.</li>
<li>Go to the &ldquo;Downloads&rdquo; section in your dashboard.</li>
<li>Generate a new configuration file under &ldquo;WireGuard Configurations.&rdquo;</li>
<li>If you&rsquo;re using torrents, enable <strong>NAT-PMP</strong>.</li>
</ul>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_e1bb7171adce48a.webp" alt="" width="560" height="368" data-zoom-src="https://www.allsubjectsmatter.nl/supercharge-your-headless-raspberry-pi-vpn-with-protonvpn-and-wireguard/images/image_hu_1345a69245a3f8c.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Save this configuration file on your Raspberry Pi:</p>
<pre class="line-numbers language-bash"><code class="language-bash"># Save the configuration file to your Raspberry Pi:
# Place it in the WireGuard directory and rename it for simplicity:
sudo nano /etc/wireguard/protonvpn.conf</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p>Once you&rsquo;ve placed the configuration file in the correct directory, verify the connection.</p>
<pre class="line-numbers language-bash"><code class="language-bash"># Verify connection to ProtonVPN:
sudo wg-quick up protonvpn

# This should give an ip address different to the one frome your isp
curl ifconfig.me 

# Verify connection to ProtonVPN:
sudo wg-quick down protonvpn

curl ifconfig.me </code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>If these two ip addresses from ifconfig.me are different your are connected to a VPN. even if you dont know your ip address since its a DHCP address or other.</p>
<h3 id="installing-our-python-environment-for-protonss">Installing our Python environment for Protonss.</h3>
<p>When developing and running Python scripts on your Raspberry Pi, it&rsquo;s best to use a <strong>Python virtual environment</strong>. A virtual environment allows you to install and manage Python packages in an isolated setting, ensuring that your system’s default Python configuration remains untouched.</p>
<p>This way, any changes made to your environment will only affect the project you&rsquo;re working on—such as installing specific packages like <code>requests</code>—without causing conflicts with other Python projects or system-level dependencies.</p>
<p>Here’s how you can set up and use a virtual environment for our script:</p>
<ol>
<li><strong>Install virtual environment</strong>: First, install the <code>python3-venv</code> package, which allows you to create and manage virtual environments.</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt install -y python3-venv python3-full</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol start="2">
<li><strong>Create a virtual environment</strong>: Once installed, you can create a virtual environment. This will generate an isolated environment in your specified directory (in this case, <code>~/protonss</code>).</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">python3 -m venv ~/protonss</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ol start="3">
<li><strong>Activate the virtual environment</strong>: After creating the environment, you’ll need to activate it. This tells your system to use the Python and packages from the virtual environment, instead of the system-wide Python installation.</li>
</ol>
<pre class="line-numbers language-bash"><code class="language-bash">source ~/protonss/bin/activate</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><ul>
<li>Now that the environment is active, any Python package you install (such as <code>requests</code>) will be installed into this isolated environment.</li>
</ul>
<pre class="line-numbers language-bash"><code class="language-bash">pip install requests</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p><strong>Using this virtual environment in the future</strong>: Each time you want to run Python scripts that depend on this virtual environment, you’ll need to activate it again using the <code>source ~/venv/bin/activate</code> command. Once activated, any Python commands or packages you use will run in this environment.</p>
<h3 id="installing-our-protonss-script-to-automate-a-connection-to-the-fastest-vpn">Installing our ProtonSS script to automate a connection to the fastest VPN.</h3>
<p>We will create a configuration file to hold all the variables that the Python script will use. For example, you can define the path to your WireGuard configuration, log file, and the number of pings to test latency.</p>
<pre class="line-numbers language-bash"><code class="language-bash">cd ~/protonss</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Sample <code>config.ini</code>:</p>
<pre class="line-numbers language-bash"><code class="language-bash">nano config.ini</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-ini"><code class="language-ini">[Settings]
# Path to your WireGuard configuration file
wg_config_path = /etc/wireguard/protonvpn.conf

# Path to your log file
log_file = /var/log/wireguard_updater.log

# Number of pings to measure latency
ping_count = 3

# Maximum score difference before reconnection 0 - 100
# Average score of servers is between 10-40 it seems. so setting this to high results in no switches from servers
score_threshold = 5.0

# Comma-separated list of preferred countries
preferred_countries = US

# Comma-separated list of preferred cities
# If specified, servers must be in these cities (unless they&amp;#39;re in a country without any specified city)
# Preferred cities to connect to. If specified, servers must be in these cities
# (unless they&amp;#39;re in a country without any specified city).
# Example: preferred_cities = Phoenix, Los Angeles, Seattle
# This allows you to target specific cities within your preferred countries.
# If left empty, any city within the preferred countries will be considered.
# Note: City names are case sensitive and must match ProtonVPN&amp;#39;s naming exactly.
# Multiple cities should be comma-separated.
preferred_cities =Phoenix

# Weight for latency in the server score
# latency is subjective since this is probably measured through the vpn and only counts as server latency.
# Checking the latency without a vpn isnt recommended since the proton servers wont reply back without vpn.
# Value cant be higher then 1. 0.3 translates to 30% of the score_treshold.
latency_weight = 0.3

# Weight for server load in the server score
# Load is the best factor 0.7 translates to 70% of the weight of the score_threshold. this cant be higher then 1.
load_weight = 0.7

# Enable or disable pinging servers (True or False)
# Since ping might not be reliable enough for you to keep on switching servers this can be disabled here.
ping_enabled = True

# Specify the interface to use for ping tests. By setting this option you can ping to the fastest vpn server
# this will ensure the lowest latency VPN from your location.
ping_interface = eth0

[Features]
# Choose which type of servers you want to use
# Options: p2p, plus, secure_core, tor
# Default is p2p if not specified
feature_type = p2p

# Feature explanation:
# Each server has exactly ONE feature number:
# 1, 17 = Secure Core servers
# 2 = TOR servers
# 0, 8, 16, 24 = Plus-only servers
# 4, 12, 20, 28 = P2P servers (these are also Plus servers)</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>You can customize the variables as needed. For example, leaving <code>preferred_countries</code> empty will make the script check a LOT! of servers, which will make the script run a very very long time!</p>
<h4 id="create-the-python-script"><strong>Create the Python script</strong></h4>
<p>Now, we are going to create the Python script that will automate your VPN connection. We first need to prepare a few steps</p>
<p>The first line of the script should be a <strong>shebang</strong>, which ensures the script always runs in the correct Python environment:</p>
<pre class="line-numbers language-bash"><code class="language-bash">source ~/protonss/bin/activate
which python</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>This will provide you with the pad you need to put on the first rule of this script</p>
<pre class="line-numbers language-text"><code class="language-text">/home/pi/venv/bin/python</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Modify the script by adding a <strong>shebang</strong>: Add the following line at the very top of your Python script. Replace <code>/home/{user}/venv/bin/python</code> with the actual path you got when you ran the <code>which python</code> command earlier. Make sure to keep the <code>#!</code> at the beginning of the line—this tells the system which interpreter to use to run the script. we need the absolute path so ~/ wont work.</p>
<pre class="line-numbers language-bash"><code class="language-bash">nano protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-python"><code class="language-python">#!/home/{user}/protonss/bin/python

import requests
import subprocess
import os
import sys
import configparser
import logging
import re
from datetime import datetime
import math
import shutil

# Get the directory of the current script
script_dir = os.path.dirname(os.path.abspath(__file__))

# Load the config file from the same directory as the script
config = configparser.ConfigParser()
config.read(os.path.join(script_dir, &amp;#39;config.ini&amp;#39;))

WG_CONFIG_PATH = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;wg_config_path&amp;#39;)
LOG_FILE = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;log_file&amp;#39;)
PING_COUNT = config.getint(&amp;#39;Settings&amp;#39;, &amp;#39;ping_count&amp;#39;)
SCORE_THRESHOLD = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;score_threshold&amp;#39;, fallback=5.0)
PREFERRED_COUNTRIES = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;preferred_countries&amp;#39;, fallback=&amp;#39;&amp;#39;).split(&amp;#39;,&amp;#39;)
PREFERRED_CITIES = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;preferred_cities&amp;#39;, fallback=&amp;#39;&amp;#39;).split(&amp;#39;,&amp;#39;)
PING_INTERFACE = config.get(&amp;#39;Settings&amp;#39;, &amp;#39;ping_interface&amp;#39;, fallback=None)

# Weighting factors for latency and load from the config
LATENCY_WEIGHT = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;latency_weight&amp;#39;, fallback=0.3)
LOAD_WEIGHT = config.getfloat(&amp;#39;Settings&amp;#39;, &amp;#39;load_weight&amp;#39;, fallback=0.7)

# Ping option from config
PING_ENABLED = config.getboolean(&amp;#39;Settings&amp;#39;, &amp;#39;ping_enabled&amp;#39;, fallback=True)

# Clean up country codes (remove spaces and convert to uppercase)
PREFERRED_COUNTRIES = [country.strip().upper() for country in PREFERRED_COUNTRIES if country.strip()]

# Clean up city names (remove spaces)
PREFERRED_CITIES = [city.strip() for city in PREFERRED_CITIES if city.strip()]

# Set up logging
logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format=&amp;#39;%(asctime)s [%(levelname)s] %(message)s&amp;#39;,
    datefmt=&amp;#39;%Y-%m-%d %H:%M:%S&amp;#39;
)

logging.info(&amp;#39;--- ProtonVPN Auto Connect Script Start ---&amp;#39;)

# Feature definitions
FEATURE_SECURE_CORE = [1, 17]  # Secure Core servers
FEATURE_TOR = [2]         # TOR servers
FEATURE_PLUS = [0, 8, 16, 24]  # Plus-only servers
FEATURE_P2P = [4, 12, 20, 28]    # P2P servers (these are also Plus servers)
# check https://api.protonmail.ch/vpn/logicals for more info. 
# be aware that the secure core and tor server are very limited so check if one is available for your country as stated in the config.ini file.
# FREE servers are not included in the api response. so they are already left out.

def get_feature_description(features):
    # If no feature type is specified in config.ini, default to P2P
    # Features is a number representing server capabilities
    if not features:  # If features is 0 or None
        return &amp;#34;P2P&amp;#34;  # Default to P2P
    elif features in FEATURE_SECURE_CORE:
        return &amp;#34;Secure Core&amp;#34;
    elif features in FEATURE_TOR:
        return &amp;#34;TOR&amp;#34; 
    elif features in FEATURE_P2P:
        return &amp;#34;P2P&amp;#34;
    elif features in FEATURE_PLUS:
        return &amp;#34;Plus&amp;#34;
    else:
        return &amp;#34;P2P&amp;#34;  # Default to P2P for any unrecognized feature

def get_wanted_features():
    &amp;#34;&amp;#34;&amp;#34;Get the wanted feature type based on config priorities&amp;#34;&amp;#34;&amp;#34;
    feature_type = config.get(&amp;#39;Features&amp;#39;, &amp;#39;feature_type&amp;#39;, fallback=&amp;#39;p2p&amp;#39;).lower()
    
    if feature_type == &amp;#39;secure_core&amp;#39;:
        return FEATURE_SECURE_CORE
    elif feature_type == &amp;#39;tor&amp;#39;:
        return FEATURE_TOR
    elif feature_type == &amp;#39;plus&amp;#39;:
        return FEATURE_PLUS
    else:  # Default to P2P
        return FEATURE_P2P

# Function to fetch the list of servers from the ProtonVPN API
def get_servers(full_list=False):
    try:
        url = &amp;#39;https://api.protonmail.ch/vpn/logicals&amp;#39;
        response = requests.get(url)
        response.raise_for_status()
        data = response.json()
        logging.info(&amp;#34;API response data received.&amp;#34;)
        return data[&amp;#39;LogicalServers&amp;#39;]
    except requests.RequestException as e:
        logging.error(f&amp;#39;Error fetching server list: {e}&amp;#39;)
        sys.exit(1)
    except ValueError as e:
        logging.error(f&amp;#39;Error parsing JSON: {e}&amp;#39;)
        sys.exit(1)

# Function to filter P2P servers and servers in preferred countries
def filter_servers(servers):
    filtered_servers = []
    
    # Get feature preferences from config (nu maar één type)
    wanted_features = get_wanted_features()
    
    logging.info(f&amp;#34;Filtering servers with preferences - Countries: {PREFERRED_COUNTRIES or &amp;#39;Any&amp;#39;}, &amp;#34;
                f&amp;#34;Cities: {PREFERRED_CITIES or &amp;#39;Any&amp;#39;}, Features: {[get_feature_description(f) for f in wanted_features]}&amp;#34;)
    
    for server in servers:
        if not isinstance(server, dict):
            logging.error(f&amp;#34;Unexpected type for server: {type(server)} - Value: {server}&amp;#34;)
            continue
            
        features = server.get(&amp;#39;Features&amp;#39;, 0)
        country = server[&amp;#39;ExitCountry&amp;#39;]
        city = server.get(&amp;#39;City&amp;#39;, &amp;#39;&amp;#39;)

        # Check if server features match our wanted features
        if features in wanted_features:
            # Check country and city preferences
            if country in PREFERRED_COUNTRIES:
                if not PREFERRED_CITIES or city in PREFERRED_CITIES:
                    filtered_servers.append(server)
                    logging.info(f&amp;#34;Server matched - Country: {country}, City: {city}, &amp;#34;
                              f&amp;#34;Features: {get_feature_description(features)}&amp;#34;)
            
    if not filtered_servers:
        logging.warning(f&amp;#34;No servers found matching filters. Countries: {PREFERRED_COUNTRIES}, &amp;#34;
                      f&amp;#34;Cities: {PREFERRED_CITIES}, Features: {[get_feature_description(f) for f in wanted_features]}&amp;#34;)
    else:
        logging.info(f&amp;#34;Found {len(filtered_servers)} matching servers&amp;#34;)
        
    return filtered_servers

# Function to measure latency by pinging the server (optional)
def get_latency(hostname):
    if not PING_ENABLED:
        logging.info(f&amp;#34;Ping disabled, skipping latency check for {hostname}.&amp;#34;)
        return 0  # Return 0 latency if ping is disabled

    latencies = []
    for i in range(PING_COUNT):
        try:
            # Add the interface option if it&amp;#39;s specified
            ping_command = [&amp;#39;ping&amp;#39;, &amp;#39;-c&amp;#39;, &amp;#39;1&amp;#39;, &amp;#39;-W&amp;#39;, &amp;#39;1&amp;#39;, hostname]
            if PING_INTERFACE:
                ping_command.insert(1, &amp;#39;-I&amp;#39;)
                ping_command.insert(2, PING_INTERFACE)

            result = subprocess.run(ping_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if result.returncode == 0:
                output = result.stdout.decode()
                latency_line = [line for line in output.split(&amp;#39;\n&amp;#39;) if &amp;#39;time=&amp;#39; in line][0]
                latency = float(latency_line.split(&amp;#39;time=&amp;#39;)[1].split(&amp;#39; ms&amp;#39;)[0])
                latencies.append(latency)
            else:
                latencies.append(float(&amp;#39;inf&amp;#39;))
        except Exception as e:
            latencies.append(float(&amp;#39;inf&amp;#39;))
            logging.error(f&amp;#39;Ping error for {hostname}: {e}&amp;#39;)

    # Return the lowest (best) latency value
    best_latency = min(latencies)
    
    # Handle inf latency
    if math.isinf(best_latency):
        logging.warning(f&amp;#34;Latency for {hostname} is inf. Assigning high latency value.&amp;#34;)
        best_latency = 1000  # Assign a high latency value

    return best_latency

# Function to select the best server based on a weighted combination of latency and load
def select_best_server(servers):
    best_server = None
    best_score = float(&amp;#39;inf&amp;#39;)
    
    for server in servers:
        server_load = server.get(&amp;#39;Load&amp;#39;)
        
        # Handle inf load
        if server_load is None or math.isinf(server_load):
            logging.warning(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} has inf load. Assigning high load value.&amp;#34;)
            server_load = 100  # Assign a high load value if it&amp;#39;s inf or None
        else:
            server_load = int(server_load)
        
        for srv in server[&amp;#39;Servers&amp;#39;]:
            exit_ip = srv.get(&amp;#39;ExitIP&amp;#39;)
            hostname = exit_ip

            # Get latency, only if ping is enabled
            latency = get_latency(hostname)

            # Combine latency and load using the configured weights
            score = (latency * LATENCY_WEIGHT) &#43; (server_load * LOAD_WEIGHT)
            
            logging.info(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} - Latency: {latency:.2f} ms, Load: {server_load}%, Score: {score:.2f}&amp;#34;)
            
            if score &amp;lt; best_score:
                best_score = score
                best_server = server
                best_server_instance = srv

    return best_server, best_server_instance, best_score

# Function to read the current server IP from the WireGuard configuration
def get_current_server_ip():
    try:
        with open(WG_CONFIG_PATH, &amp;#39;r&amp;#39;) as f:
            config_text = f.read()
        # Extract the current Endpoint IP
        match = re.search(r&amp;#39;Endpoint\s*=\s*([\d\.]&#43;):\d&#43;&amp;#39;, config_text)
        if match:
            logging.info(f&amp;#34;Current server IP found in WireGuard configuration: {match.group(1)}&amp;#34;)
            return match.group(1)
        else:
            logging.warning(&amp;#39;No current Endpoint IP found in WireGuard configuration.&amp;#39;)
            return None
    except Exception as e:
        logging.error(f&amp;#39;Error reading the WireGuard configuration: {e}&amp;#39;)
        return None

# Function to find the current server&amp;#39;s performance in the live server data
def get_current_server_performance(servers, current_ip):
    if current_ip is None:
        logging.warning(&amp;#39;Current server IP is None, skipping performance check.&amp;#39;)
        return None, None, float(&amp;#39;inf&amp;#39;)
    
    for server in servers:
        for srv in server[&amp;#39;Servers&amp;#39;]:
            if srv[&amp;#39;ExitIP&amp;#39;] == current_ip:
                server_load = server.get(&amp;#39;Load&amp;#39;)
                if server_load is None or math.isinf(server_load):
                    logging.warning(f&amp;#34;Server {server[&amp;#39;Name&amp;#39;]} (IP: {current_ip}) has inf load. Assigning high load value.&amp;#34;)
                    server_load = 100  # Assign a high load value
                else:
                    server_load = int(server_load)

                latency = get_latency(current_ip) if PING_ENABLED else 0
                score = (latency * LATENCY_WEIGHT) &#43; (server_load * LOAD_WEIGHT)

                logging.info(f&amp;#34;Current server {server[&amp;#39;Name&amp;#39;]} - Latency: {latency:.2f} ms, Load: {server_load}%, Score: {score:.2f}&amp;#34;)
                return server, srv, score
    logging.warning(f&amp;#34;Current server with IP {current_ip} not found in live data.&amp;#34;)
    return None, None, float(&amp;#39;inf&amp;#39;)

# Function to find the current server in the full ProtonVPN list if not found in filtered data
def find_current_server_full_list(current_ip):
    servers = get_servers(full_list=True)
    return get_current_server_performance(servers, current_ip)

# Function to update the WireGuard configuration file
def update_wg_config(server, server_instance, best_score):
    try:
        endpoint_ip = server_instance[&amp;#39;EntryIP&amp;#39;]
        endpoint_port = 51820  # Default WireGuard port
        endpoint = f&amp;#34;{endpoint_ip}:{endpoint_port}&amp;#34;
        public_key = server_instance[&amp;#39;X25519PublicKey&amp;#39;]
        server_name = server[&amp;#39;Name&amp;#39;]

        # Read the current WireGuard configuration
        with open(WG_CONFIG_PATH, &amp;#39;r&amp;#39;) as f:
            config_text = f.read()

        # Strip comments that start with # under [Peer] before PublicKey
        config_text = re.sub(r&amp;#39;#.*\n&amp;#39;, &amp;#39;&amp;#39;, config_text, flags=re.M)

        # Update the Endpoint in the configuration
        config_text = re.sub(r&amp;#39;Endpoint\s*=.*&amp;#39;, f&amp;#39;Endpoint = {endpoint}&amp;#39;, config_text)

        # Update the PublicKey in the configuration
        config_text = re.sub(r&amp;#39;PublicKey\s*=.*&amp;#39;, f&amp;#39;PublicKey = {public_key}&amp;#39;, config_text)

        # Add the new comment indicating the chosen server and score
        config_text = re.sub(r&amp;#39;\[Peer\]\n&amp;#39;, f&amp;#39;[Peer]\n# {server_name} - Auto-generated by ProtonVPN Auto Connect - Score: {best_score:.2f}\n&amp;#39;, config_text)

        # Write the updated configuration back to the file
        with open(WG_CONFIG_PATH, &amp;#39;w&amp;#39;) as f:
            f.write(config_text)
        os.chmod(WG_CONFIG_PATH, 0o600)

        logging.info(f&amp;#34;WireGuard configuration updated with server: {server_name} ({server[&amp;#39;ExitCountry&amp;#39;]}), Endpoint: {endpoint}, PublicKey: {public_key}, Score: {best_score:.2f}&amp;#34;)
    except Exception as e:
        logging.error(f&amp;#39;Error updating the WireGuard configuration: {e}&amp;#39;)
        sys.exit(1)

# Function to restart the WireGuard interface
def is_resolvconf_available():
    &amp;#34;&amp;#34;&amp;#34;Check if resolvconf is available on the system.&amp;#34;&amp;#34;&amp;#34;
    return shutil.which(&amp;#34;resolvconf&amp;#34;) is not None

def is_wg_quick_available():
    &amp;#34;&amp;#34;&amp;#34;Check if wg-quick is available on the system.&amp;#34;&amp;#34;&amp;#34;
    return shutil.which(&amp;#34;wg-quick&amp;#34;) is not None

def restart_wg():
    interface_name = os.path.basename(WG_CONFIG_PATH).split(&amp;#39;.&amp;#39;)[0]
    
    # Check if wg-quick is available before proceeding
    if not is_wg_quick_available():
        logging.error(&amp;#34;wg-quick not found. Ensure WireGuard is installed correctly.&amp;#34;)
        sys.exit(1)

    try:
        # Check if resolvconf is available before proceeding
        if not is_resolvconf_available():
            logging.warning(&amp;#34;resolvconf not found, DNS settings may not be updated correctly.&amp;#34;)

        # Bring down the interface
        subprocess.run([&amp;#39;wg-quick&amp;#39;, &amp;#39;down&amp;#39;, interface_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        # Bring up the interface
        subprocess.run([&amp;#39;wg-quick&amp;#39;, &amp;#39;up&amp;#39;, interface_name], check=True)
        logging.info(f&amp;#39;WireGuard interface {interface_name} started.&amp;#39;)

    except subprocess.CalledProcessError as e:
        logging.error(f&amp;#39;Error starting the WireGuard interface: {e}&amp;#39;)
        sys.exit(1)

# Main function
def main():
    logging.info(&amp;#39;--- Starting server selection process ---&amp;#39;)
    
    # Fetch the list of servers
    servers = get_servers()
    if not isinstance(servers, list):
        logging.error(f&amp;#39;Unexpected type for servers: {type(servers)}&amp;#39;)
        sys.exit(1)

    # Filter the servers
    filtered_servers = filter_servers(servers)
    if not filtered_servers:
        logging.error(&amp;#39;No suitable servers available after filtering.&amp;#39;)
        sys.exit(1)

    # Get the current server&amp;#39;s IP address from the WireGuard configuration
    current_ip = get_current_server_ip()

    # Get the current server&amp;#39;s performance if it exists
    current_server, current_server_instance, current_score = get_current_server_performance(filtered_servers, current_ip)

    # If the current server is not found, check the full list of servers
    if current_server is None:
        logging.info(f&amp;#34;Current server not found in filtered list, checking full server list...&amp;#34;)
        current_server, current_server_instance, current_score = find_current_server_full_list(current_ip)

    # Select the best server based on weighted latency and load
    best_server, best_server_instance, best_score = select_best_server(filtered_servers)
    if not best_server:
        logging.error(&amp;#39;No suitable server found.&amp;#39;)
        sys.exit(1)

    if current_server:
        logging.info(f&amp;#34;Current server {current_server[&amp;#39;Name&amp;#39;]} (IP: {current_ip}) - Score: {current_score:.2f}&amp;#34;)
    
    logging.info(f&amp;#34;Selected best server {best_server[&amp;#39;Name&amp;#39;]} - Score: {best_score:.2f}&amp;#34;)
    
    if current_server and current_score &amp;lt;= best_score &#43; SCORE_THRESHOLD:
        logging.info(f&amp;#34;Current server {current_server[&amp;#39;Name&amp;#39;]} is within threshold. No switch necessary.&amp;#34;)
    else:
        logging.info(f&amp;#34;Switching to server {best_server[&amp;#39;Name&amp;#39;]} due to better performance.&amp;#34;)
        update_wg_config(best_server, best_server_instance, best_score)
        restart_wg()

    logging.info(&amp;#39;--- ProtonVPN Auto Connect Script End ---&amp;#39;)

if __name__ == &amp;#39;__main__&amp;#39;:
    main()</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h3 id="testing-the-script">Testing the script</h3>
<pre class="line-numbers language-bash"><code class="language-bash">chmod &#43;x protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Once the script is saved, give it execution permissions and run it</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo ./protonvpn_auto_connect.py</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">cat /var/log/wireguard_updater.log
tail -f /var/log/wireguard_updater.log</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>you can tail or cat the log file to check its output in the logs. Tail is preferred since you can keep watching it while running your script. Just open a second ssh with the tail to keep reviewing it.</p>
<pre class="line-numbers language-log"><code class="language-log">2024-09-25 22:19:10 [INFO] --- ProtonVPN Auto Connect Script Start ---
2024-09-25 22:19:10 [INFO] --- Starting server selection process ---
2024-09-25 22:19:11 [INFO] API response data received.
2024-09-25 22:19:11 [INFO] Current server IP found in WireGuard configuration: 79.135.105.20
2024-09-25 22:19:11 [WARNING] Current server with IP 79.135.105.20 not found in filtered data.
2024-09-25 22:19:11 [INFO] Current server not found in filtered list, checking full server list...
2024-09-25 22:19:12 [INFO] API response data received.
2024-09-25 22:19:13 [INFO] Current server BH#1 - Latency: 26.90 ms, Load: 10%, Score: 15.07
2024-09-25 22:19:14 [INFO] Server AO#1 - Latency: 27.10 ms, Load: 7%, Score: 13.03
2024-09-25 22:19:15 [INFO] Server AO#2 - Latency: 26.60 ms, Load: 14%, Score: 17.78
2024-09-25 22:19:16 [INFO] Server AO#3 - Latency: 27.10 ms, Load: 9%, Score: 14.43
2024-09-25 22:19:17 [INFO] Server AO#4 - Latency: 26.60 ms, Load: 16%, Score: 19.18
2024-09-25 22:19:17 [INFO] Current server BH#1 (IP: 79.135.105.20) - Score: 15.07
2024-09-25 22:19:17 [INFO] Selected best server AO#1 - Score: 13.03
2024-09-25 22:19:17 [INFO] Current server BH#1 is within threshold. No switch necessary.
2024-09-25 22:19:17 [INFO] --- ProtonVPN Auto Connect Script End ---</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">sudo cat /etc/wireguard/protonvpn.conf</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>You can also verify that everything works by checkin the protonvpn.conf file or by checking your external ip address.</p>
<pre class="line-numbers language-ini"><code class="language-ini">[Peer]
# BH#3 - Auto-generated by ProtonVPN Auto Connect - Score: 7.74</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>you will see something like this above. This comment will update when it connects to another VPN and the ipaddress inside the peer also changes with it off course.</p>
<pre class="line-numbers language-bash"><code class="language-bash">curl ifconfig.me #this wil give the external ip in the cli.</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>As an additional step you can also check your external ip. this should be something different then the ip address provided by your provider.</p>
<h3 id="running-this-script-every-hour">Running this script every hour.</h3>
<pre class="line-numbers language-bash"><code class="language-bash">sudo crontab -e</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-text"><code class="language-text">0 * * * * cd /home/{user}/protonss &amp;amp;&amp;amp; /home/{user}/protonss/protonvpn_auto_connect.py &amp;gt;&amp;gt; /home/{user}/protonss/protonvpn_cron.log 2&amp;gt;&amp;amp;1</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Here we defined it to run every hour. but can you schedule it how you want.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo crontab -l</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Save the crontab and verify the cron is installed.</p>
<h3 id="optional-block-specific-traffic-if-your-vpn-fails">Optional: Block specific traffic if your VPN fails</h3>
<p>If you want to ensure no traffic leaves your device if the VPN connection fails, you can use <code>iptables</code> to block traffic when the WireGuard interface is down.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt-get install iptables</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Verify that you have installed iptables or install it with the above command.</p>
<pre class="line-numbers language-bash"><code class="language-bash">ifconfig -a</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><pre class="line-numbers language-text"><code class="language-text">protonvpn: flags=209&amp;lt;UP,POINTOPOINT,RUNNING,NOARP&amp;gt;  mtu 1420
        inet 10.2.1.3  netmask 255.255.255.255  destination 10.2.1.3
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1000  (UNSPEC)
        RX packets 612  bytes 101768 (99.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 765  bytes 101144 (98.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>First check the name of your network interface. If you named your file protonvpn.conf it should be named protonvpn.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo iptables -A OUTPUT -p tcp --dport 51413 ! -o protonvpn -j REJECT --reject-with icmp-port-unreachable
sudo iptables -A OUTPUT -p udp --dport 51413 ! -o protonvpn -j REJECT --reject-with icmp-port-unreachable</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span></span></pre><p>you can add any rule to your iptables. The 2 rules below make sure this port 51413 is rejected when the network interface called protonvpn isn&rsquo;t available. There is a rule for UDP and TCP traffic in place.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo iptables -L OUTPUT -v -n</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>After adding all your required rules you can verify them with this command .</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt-get install iptables-persistent</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>And make these rules persistent so you dont have to add them on every reboot.</p>
<pre class="line-numbers language-bash"><code class="language-bash">sudo netfilter-persistent save</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><h3 id="conclusion">Conclusion</h3>
<p>With this Python script, your Raspberry Pi will always be connected to the fastest ProtonVPN server available, ensuring you have the best possible VPN performance without manual intervention. Whether you&rsquo;re using your Pi for networking, torrenting, or general online security, this automation ensures optimal performance and reliable privacy protection.</p>
<h3 id="update20-12-2024">Update - 20-12-2024</h3>
<p>Added Preferred_cities as an option to shorten the US country select. That included over 2000 servers to check otherwise. The performance check can at least get a bit shorter this way :-).</p>
<p>And i found out what each Feature section for every server in the API includes. I will explain this below. I adjusted my code accordingly to work with this correctly. Be aware that the config you made earlier should reflect the type of server you are connecting to. If your Configuration isnt configured to connect to a Secure Core dont select a Secure Core in this script also. Reflect to the created ProtonVPN Configuration.</p>
<p>Feature explanation:</p>
<p>Each server has exactly ONE feature number:</p>
<ul>
<li>1, 17 = Secure Core servers</li>
<li>2 = TOR servers</li>
<li>0, 8, 16, 24 = Plus-only servers</li>
<li>4, 12, 20, 28 = P2P servers (these are also Plus servers it seems)</li>
</ul>
<pre class="line-numbers language-json"><code class="language-json">        {
            &amp;#34;Name&amp;#34;: &amp;#34;US-AZ#105&amp;#34;,
            &amp;#34;EntryCountry&amp;#34;: &amp;#34;US&amp;#34;,
            &amp;#34;ExitCountry&amp;#34;: &amp;#34;US&amp;#34;,
            &amp;#34;Domain&amp;#34;: &amp;#34;node-us-260.protonvpn.net&amp;#34;,
            &amp;#34;Tier&amp;#34;: 2,
            &amp;#34;Features&amp;#34;: 12,</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>After some more investigation regarding questions about the Free servers from ProtonVPN, it appears that ProtonVPN has removed public API access to their free server endpoints. The /vpn/servers endpoint that previously should have provided information about free servers now returns a 404 error:</p>
<pre class="line-numbers language-json"><code class="language-json">{    &amp;#34;Code&amp;#34;: 404,    &amp;#34;Error&amp;#34;: &amp;#34;Path not found&amp;#34;,    &amp;#34;Details&amp;#34;: {}}</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Currently, only the Plus server endpoint is accessible via the public</p>
<p>API:url = &lsquo;<a href="https://api.protonmail.ch/vpn/logicals" data-external>https://api.protonmail.ch/vpn/logicals</a>&rsquo;</p>
<p>This is likely a deliberate change by ProtonVPN. So sadly this script cant support the Free servers.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Azure &#43; MySQL, MariaDB, PostgreSQL performance compared</title>
      <link>https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/</link>
      <guid isPermaLink="false">6605e7eafc993e0001aef0ea</guid>
      <pubDate>Fri, 29 Mar 2024 00:04:45 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Exploring Azure&#39;s database performance while on a journey through MySQL, MariaDB, and PostgreSQL. Find out which database leads the race.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h3 id="introduction">Introduction</h3>
<p>This post aims to shed light on database performance within Azure, focusing on a direct comparison between MySQL, MariaDB, and PostgreSQL. Despite abundant literature on the subject, practical tests that measure real-life performance are scarce.</p>
<p>Many discussions revolve around the differences between these databases rather than their actual performance. When I started this research, my priority wasn&rsquo;t the differences but finding a performant database that could serve as a viable Azure-based alternative for our MariaDB Monolithic setup. Initially, we thought switching from MariaDB to MySQL was a straightforward replacement with almost no performance impact. We were wrong there!</p>
<blockquote class="pull-quote"><p>&ldquo;There are many opinions about performance, But we needed facts&rdquo;</p>
</blockquote>
<p>We encountered a massive regression in performance in every query. Performance is crucial to us, and it was perplexing to see MySQL lag behind MariaDB so much. Upon discovering this, we set up a MariaDB VM ourselves in Azure, which proved to be performant again but introduced a lot of additional maintenance and management challenges, such as ensuring uptime, hot standby, and backups.</p>
<p>This led me to explore PostgreSQL, hoping for better performance than what MySQL offered. With this introduction, let&rsquo;s dive into the tests we conducted, including a complex query and a set of small queries, to test the performance differences between the three databases and their implementations within Azure. We also included Docker tests to be able to determine whether the performance discrepancies are database-related or if Azure is the contributing factor.</p>
<h2 id="testing-our-complex-query">Testing our complex Query</h2>
<p>To assess the databases, we ran a complex query that aggregated data from multiple tables, representing a realistic workload for our system. For confidentiality reasons, we&rsquo;ve altered table and column names in this description. This query aimed to evaluate how each database handles data aggregation and complex joins.</p>
<pre class="line-numbers language-sql"><code class="language-sql">WITH AggregatedSkateData AS (
SELECT
    e.id AS eventID,
    e.theme_cid AS themeCID,
    s.id AS skateID,
    p.firstName || &amp;#39; &amp;#39; || p.lastName AS participantName,
    l.label AS locationLabel,
    s.priceOffered,
    s.inspectionCompleted,
    s.registrationDate,
    s.eventStartDate,
    org.name AS organizerName,
    &amp;#39;https://skateeventimages.net/&amp;#39; || im.path AS organizerImageURL,
    s.registrationCode,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 0 OR pa_last.id IS NULL THEN sp.id END) AS partsPending,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 1 THEN sp.id END) AS partsRequested,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 2 THEN sp.id END) AS partsConfirmed,
    COUNT(DISTINCT CASE WHEN pa_last.status_cid = 8 THEN sp.id END) AS partsDenied,
    MAX(CASE WHEN ti_unscanned.id IS NOT NULL THEN 1 ELSE 0 END) AS hasUnscannedTickets,
    s.isPublic
FROM
    skateEvents s
    INNER JOIN events e ON s.eventID = e.id
    INNER JOIN participants p ON p.id = e.participantID
    LEFT JOIN organizers org ON org.id = e.organizerID
    LEFT JOIN images im ON im.id = org.profileImageID
    LEFT JOIN locations l ON l.id = s.locationID
    LEFT JOIN skateParts sp ON sp.eventID = s.id
    LEFT JOIN partApprovals pa ON pa.id = sp.partApprovalID
    LEFT JOIN ticketsUnscanned ti_unscanned ON ti_unscanned.id = sp.ticketID AND ti_unscanned.scannedAt IS NULL
    LEFT JOIN partApprovalActions pa_last ON pa_last.id = (
        SELECT pa2.id
        FROM partApprovalActions pa2
        WHERE pa2.partID = pa.id
        ORDER BY pa2.id DESC
        LIMIT 1
    )
WHERE
    s.registrationDate IS NOT NULL
    AND e.cancelledAt IS NULL
    AND e.deletedAt IS NULL
    AND s.isArchived = false
    AND s.deletedAt IS NULL
GROUP BY
    e.id, e.theme_cid, s.id, p.id, p.firstName, p.lastName, org.id, im.id, l.id, l.label, s.priceOffered, s.inspectionCompleted, s.registrationDate, s.eventStartDate, org.name, im.path, s.registrationCode, s.isPublic
)
SELECT * FROM AggregatedSkateData
WHERE
    TO_CHAR(s.registrationDate, &amp;#39;YYYY-MM&amp;#39;) = &amp;#39;2024-03&amp;#39;
    OR partsDenied &#43; partsRequested &#43; partsPending &amp;gt; 0
ORDER BY s.registrationDate DESC</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h3 id="azure-complicated-query">Azure, complicated query</h3>
<p>We conducted these tests within the same Azure zone for containers and databases to avoid interzone latency. Below are the specifications and results for transparency. It&rsquo;s worth noting that for MariaDB and MySQL, the performance specifications didn&rsquo;t markedly affect outcomes, provided we avoided the first (burstable) tier. PostgreSQL was the exception, showing noticeable improvements in large query runtimes.</p>
<p>Using HeidiSQL via the Azure VPN, we determined our best results out of five runs.</p>
<p><strong>MySQL Flexible, Zone 2 (Standard_E4ds_v5, 450 iops)</strong><br>
Found rows: 130<br>
Duration for 1 query: 13,875 sec</p>
<p><strong>MariaDB VM, Zone 2 (Standard B4s v2 (4 vcpus, 16 GiB memory))</strong><br>
Found rows: 130<br>
Duration for 1 query: 10,625 sec<br>
23.4% better then MySQL</p>
<p><strong>Postgres Flexible, Zone 3 (Standard_E4ds_v50 p10, 500 iops)</strong><br>
Found rows: 130<br>
Duration for 1 query: 3,953 sec<br>
71.5% better then MySQL</p>
<h3 id="local-docker-complicated-query">Local Docker, complicated query</h3>
<p>Local hardware we tested on</p>
<p>12th Gen Intel(R) Core(TM) i9-12900H, 2900 Mhz, 14 Core(s), 20 Logical Processor(s)<br>
Installed Physical Memory (RAM) 32,0 GB</p>
<p><strong>MySQL 8.3 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 12,437 sec</p>
<p><strong>MariaDB 11.4 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 9,484 sec</p>
<p><strong>Postgres 16.2 Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 3,969 sec</p>
<p>This concludes our complicated query tests. It is awesome to see Postgres perform so well here! We used to run in MariaDB before we started our move to Azure. It was very sad to see such bad performance in MySQL the moment we started testing with this new setup.</p>
<h2 id="testing-our-set-small-querys">Testing our set small Query&rsquo;s</h2>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_c11062909b9cd6ee.webp" alt="" width="560" height="171" data-zoom-src="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/images/image-2_hu_44990348cc6259d6.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>The next test is when we fire more then 20.000 transactions per second against each database which will build us a feed in JSON that takes a certain amount of time to complete. we stress our database better with a lot of query&rsquo;s which will give us a average runtime for every database.<br>
There is also some PHP logic here since this is a PHP script we run to generate the feed. But it runs on the same hardware/docker image all the time. so the difference in runtime comes from the database.<br>
For this we moved our containers to the same zone as our database is running to also take away potential issues with zone latency.</p>
<p>We are running this script within our Laravel framework. We are running our PHP against <a href="https://openswoole.com/" data-external>Swoole</a> for the best possible performance within PHP 8.</p>
<h3 id="azure-small-querys">Azure, small query&rsquo;s</h3>
<p>In this test we also introduce container performance aspects .<br>
We run our containers and database again in the same Zone in Azure (Western Europe). When using Azure we run with a lot less performance than we have available in our docker environment.</p>
<p>For this we can only use these results to compare the differences within docker and Azure itself, not between them. Comparing the two together wont mean anything due to this hardware difference. We only want to observe the difference in runtime against the databases. and Docker is a baseline to check if the differences match up with the differences we see in Azure.</p>
<p>Our Node Size in Azure is a standard_b2s, 2 vCPUs with 4 GiB of RAM</p>
<p><strong>API, Zone 2 -&gt; MariaDB VM, Zone 2</strong><br>
24.59s runtime (spot nodes)</p>
<p><strong>API, Zone 2 -&gt; MySQL Flexible, Zone 2</strong><br>
27.87s runtime (spot nodes)</p>
<p><strong>API, Zone 3 -&gt; Postgres Flexible, Zone 3</strong><br>
24.92s runtime (spot nodes)</p>
<h3 id="local-docker-small-querys">Local Docker, small query&rsquo;s</h3>
<p>For the small query tests within local Docker we utilize a system powered by a 12th Gen Intel(R) Core(TM) i9-12900H CPU, running at 2900 MHz with 14 cores and 20 logical processors, complemented by 32 GB of installed RAM.</p>
<p>These Docker images are configured to be memory-bound, meaning their performance was primarily dependent on the available memory rather than being limited by the CPU. The CPU settings were configured to allow unrestricted performance, ensuring that Docker can utilize as much CPU resource as needed.</p>
<p>Before each test run against a Database, I made sure to restart the Docker container to refresh its connection. After restarting, I allowed a 30-second pause to ensure that the container had fully stabilized and was running idle. This pause is crucial for ensuring more consistent test conditions. Once the container was ready, I initiated the feed generator script to begin building the feed, assessing how effectively our different database setups could handle small queries under these controlled conditions.</p>
<p><strong>API -&gt; MariaDB 11.4 Docker</strong><br>
22.88s runtime</p>
<p><strong>API -&gt; MySQL 8.3 Docker</strong><br>
23.74s runtime</p>
<p><strong>API -&gt; Postgres 16.2 Docker</strong><br>
20.77s runtime</p>
<h2 id="mariadb-query-cache">MariaDB query cache</h2>
<p>In our past use of MariaDB within our monolithic setup, we made significant use of the query cache functionality. This feature played a crucial role in delivering performance for repeated queries. This efficiency persists until the cache requires rebuilding, typically triggered by updates or inserts into the cached query&rsquo;s tables. Lets do a second run on our complex query with Query cache enabled.</p>
<p><strong>Local MariaDB Docker</strong><br>
Found rows: 130<br>
Duration for 1 query: 0,000 sec</p>
<p>However, the performance benefits of the query cache aren&rsquo;t universally consistent. For instance, when executing an average of 20,000 transactions per second with small queries, the runtime dropped to some extent. This variance highlights that the query cache doesn&rsquo;t always produce the same level of optimization, particularly under high-load conditions. but more about that topic later.</p>
<p><strong>API -&gt; Local MariaDB Docker (query cache enabled)</strong><br>
17.30s runtime</p>
<p><strong>API, Zone 2 -&gt; MariaDB VM, Zone 2 (query cache enabled)</strong><br>
22.72s runtime (spot nodes)</p>
<p>The query cache in MariaDB works by storing the result set of SELECT queries, along with the query text. When an identical query is executed again (with the same text and under the same database user privileges), MariaDB can bypass the query execution phase, pulling the result directly from the cache. This feature can drastically reduce query execution times for frequently run queries that do not alter data.</p>
<p>It&rsquo;s important to approach the use of MariaDB&rsquo;s query cache with caution. Despite its benefits, the query cache was deprecated and disabled by default in newer versions of MariaDB 10.1.7 (and MySQL as of version 5.7.20) due to concerns over its impact on performance in environments with high levels of concurrent transactions. Nonetheless, for specific scenarios, particularly those with a predominant read workload, the query cache can still significantly enhance performance. It is completely removed in MySQL 8 and up.</p>
<h2 id="observation-on-azures-flexible-servers">Observation on Azure&rsquo;s Flexible Servers</h2>
<p>These tests highlight that the performance of databases on Azure&rsquo;s flexible servers has improved significantly, aligning closely with the performance seen in local Docker environments. This marks a decent improvement from the previous Azure database single servers, which typically exhibited lesser performance due to some form of overhead introduced by how it was it was running in the Azure cloud.</p>
<h2 id="azure-latency-anomaly">Azure Latency anomaly</h2>
<p>As an added bonus, we conducted some additional tests to measure interzone latency when connecting from Zone 1 or 2 to a PostgreSQL database in Zone 3 within Azure.</p>
<p><strong>API, Zone 2 -&gt; Azure PostgreSQL Flexible, Zone 3</strong><br>
The runtime was 44.23 seconds.</p>
<p>Interestingly, the runtime doubled again, a phenomenon observed exclusively when communications occur between Zones 1-2 and Zone 3, or vice versa. This pattern suggests that any container requiring a database connection experiences significant latency under these conditions, whereas latency is nearly negligible when the interaction is between Zone 1 and Zone 2.</p>
<p>For those who might have missed it, I&rsquo;ve previously discussed a latency anomaly encountered in Zone 3. The conventional wisdom suggests maintaining the database connection within the same zone to minimize latency. However, our tests show that the latency impact is minimal when operating between Zones 1 and 2, but it becomes significantly pronounced when crossing from Zones 1 or 2 to Zone 3.</p>
<p>This testing further confirms that the latency issue is not exclusive to MySQL Flexible services but also affects PostgreSQL Flexible services within Azure at least, indicating a broader pattern across Azure&rsquo;s database offerings.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/"><span class="bookmark__body"><span class="bookmark__title">Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</span><span class="bookmark__description">Unveiling a significant Azure performance issue: Zone 3’s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/mysql-mariadb-postgresql-performance-compared-in-azure/bookmarks/01-thumb_hu_27e6839ff9d3a962.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<h2 id="conclusion">Conclusion</h2>
<p>From a performance related evaluation, it&rsquo;s clear that choosing the right database is crucial and should match your app&rsquo;s specific needs for speed and efficiency. The results show MySQL tends to be slower. MariaDB works well and has a handy query cache feature, but Azure has no support for MariaDB anymore. This makes PostgreSQL the most viable choice for performance-focused users in Azure, as it excels in handling both complex and numerous simple queries effectively as shown in the tests above.</p>
<p>This analysis hopefully helps in selecting the right database technology and underlines how important careful planning is for boosting your app performance. For those using Azure and looking for top tier performance, PostgreSQL is the go-to option in my test results.</p>
<p>There are other reasons to choose your database on. But i only wanted to focus on the performance aspect in this post. This was our main concern since we are in the proces of moving from a Monolithic server to Azure. There are more things to consider when switching databases. The advantage here is that all three databases can be integrated quite easily if you use either of them.</p>
<p>Postgres is the best compatible here with its strict routines. The complicated query above is written for Postgres and runs in MariaDB without a single change. For MySQL the TO_CHAR in the last line only needs to be replaced with DATE_FORMAT.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</title>
      <link>https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/</link>
      <guid isPermaLink="false">65e6f9da5ce24800015cc7ae</guid>
      <pubDate>Tue, 05 Mar 2024 12:16:52 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Unveiling a significant Azure performance issue: Zone 3&#39;s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="introduction">Introduction</h2>
<p>As we embark on a transition from a traditional VM setup hosted at TransIP to the scalable cloud infrastructure of Microsoft Azure, our journey is as much about discovery as it is about performance. Guided by the expertise of <a href="https://www.pionative.com/" data-external>Pionative</a>, we&rsquo;re not just moving our environment; we are attempting to reinvent it in Azure, ensuring that every step we take is carefully tested and analyzed. Our goal? To ensure optimal performance in this new architecture and to uncover any potential bottlenecks or hidden issues that might lurk within our new cloud architecture as early as possible.</p>
<h2 id="azure-regions-and-zones">Azure regions and Zones</h2>
<p>Azure&rsquo;s infrastructure spans the globe, organized into regions for localized services, and further divided into zones for high availability. <strong>Regions</strong> are specific geographical areas hosting Azure services, while <strong>zones</strong> are physical locations within these regions, each with independent utilities to isolate from failures.</p>
<h2 id="the-western-europe-region">The Western Europe Region</h2>
<p>The Western Europe region in Azure is structured into three distinct availability zones. These zones provide an additional layer of redundancy and fault tolerance by physically separating data centers within the region. This setup is designed to ensure that even in the event of a complete zone failure, the services can continue to operate unaffected in the other zones, offering robust disaster recovery solutions for businesses operating in Europe. Where Zone 1,2 and 3 are exactly located geographically is unknown to me.</p>
<h2 id="performance-anomaly-in-the-western-europe-region">Performance anomaly in the Western Europe Region</h2>
<p>Our testing regime led us to a anomaly that has become the focal point of our Azure exploration for this post: a distinct latency issue within Zone 3 of the Western Europe region. This discovery wasn&rsquo;t immediate; it emerged from days of performance testing and a keen observation of patterns that deviated from our expectations. This all because we just started to learn the way’s of Azure. And it’s difference in performance as opposed to our current setup.</p>
<p>We first started to exclude our own pods, code, performance settings within the node pool. Database performance settings and such.</p>
<h2 id="methodology-and-findings">Methodology and findings</h2>
<p>Our final approach was quite thorough to verify this result. We deployed two node pools across each of Microsoft Azure&rsquo;s three zones within the Western Europe region, aiming to rule out any node-specific variances. All these pools had the exact same performance setup.</p>
<p>Through extensive testing, including a battery of database calls and running our cron jobs integral to our application&rsquo;s functionality, we noticed a consistent and surprising pattern. Irrespective of the zone&rsquo;s reported capacity or congestion levels.</p>
<blockquote>
<p>Interactions with the database were significantly slower when either the database or the connecting pods were situated in Zone 3.</p>
</blockquote>
<h2 id="performance-results">Performance results</h2>
<p>Below is our situation running a Cronjob that takes us locally within Docker around 32 seconds to run against our current setup, which isn’t comparable to MS Azure. But it did provide us a performance Matrix and goal. The script (cronjob) we ran fires around 32,000 queries to build a feed from all the products on our website. Yes, that’s a lot, but that is not the matter in this case. We wanted to uncover the performance issue. We know this script can be optimized to perform better.</p>
<p><strong>MySQL Database in Zone 2</strong><br>
Zone 1 – pool 1: 33 seconds<br>
Zone 1 – pool 2: 32 seconds</p>
<p>Zone 2 –  pool 1:  32 seconds<br>
Zone 2 –  pool 2:  32 seconds</p>
<p>Zone 3 –  pool 1: 115 seconds<br>
Zone 3 –  pool 2: 117 seconds</p>
<p>Then, to put the icing on the cake, we switched the setup with a database running in Zone 3. And see below the expected results we hoped to see to conclude this search.</p>
<p><strong>MySQL Database on Zone 3</strong><br>
Zone 1: 120 seconds<br>
Zone 2: 122 seconds<br>
Zone 3: 33 seconds</p>
<p>We started to see the same consistency every time, even when we ran less but larger database calls.</p>
<p><strong>MySQL Database on zone 2</strong><br>
16 DB queries single threaded.</p>
<p>Zone 1: Total time for queries from log: 9.57 ms<br>
Zone 2: Total time for queries from log: 9 ms<br>
Zone 3: Total time for queries from log: 49.96 ms</p>
<h2 id="website-performance-in-azure">Website performance in Azure</h2>
<p>When we run our Database in Zone 2 while having the API pod in Zone 1, the initial load time for our website decreases by at least 400 milliseconds for the first page load, compared to the configuration where the API pod is in Zone 3.</p>
<p>This means that the setup with the database in Zone 2 and the API pod in Zone 1 results in a much faster initial response time for the website compared to having the API pod located in Zone 3 and the Database in Zone 2.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_239c52cf6c075a2e.webp" alt="" width="560" height="129" data-zoom-src="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/images/image-1_hu_5c52b5671bc6fe37.webp" loading="lazy" decoding="async">
  </picture><figcaption>The initial first call performance of our website when run in an optimal setup in Azure.</figcaption></figure>

<p>There is a margin of error here, but we can conclude from these results that when you want to run your API demographically separated in two zones, there is no way you can consistently perform when using Zone 3. Zones 1 and 2 are the way to go.</p>
<p>We must be honest that this performance anomaly only happens when you have a pod with a database connection. Running our website (which has no database connection) in Zone 3 measured almost no impact here when the API pod ran in Zone 1 and the database resided in Zone 2.</p>
<h2 id="diving-deeper-in-zone-3s-latency-issues">Diving deeper in Zone 3&rsquo;s latency issues.</h2>
<p>This performance discrepancy in Zone 3 was astonishing to us. According to Microsoft&rsquo;s best practices, the physical proximity of application servers to their respective databases within the same region should minimize latency. Our findings, however, painted a different picture, suggesting that within-region zone selection could profoundly impact application performance.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/images/data-src-image-68bb083d-b85e-44ae-9919-8404530f242e_hu_f23e2d3a977b47e3.webp" alt="" width="560" height="235" data-zoom-src="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/images/data-src-image-68bb083d-b85e-44ae-9919-8404530f242e_hu_dda4b94313ff73ba.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>Our investigation was quite comprehensive, eliminating potential variables such as our own codebase, PHP environment configurations, MySQL performance, and much more. The consistent latency issues, particularly affecting PHP&rsquo;s single-threaded cron jobs, finally pointed us towards a more systemic issue within Zone 3&rsquo;s network architecture or deployment somewhere.</p>
<h2 id="collaboration-and-learning">Collaboration and learning</h2>
<p>Our journey, backed by the collaboration with <a href="https://www.pionative.com/" data-external>Pionative</a>, underscores the importance of partnership in navigating the cloud&rsquo;s complexity. As we dove deeper into this latency anomaly, our shared efforts aim not only to optimize our deployment but also to contribute our findings back to a broader Azure community.</p>
<blockquote class="pull-quote"><p>This latency impact is too large to ignore in our production environment.</p>
</blockquote>
<h2 id="implications-and-next-steps">Implications and next steps</h2>
<p>This anomaly in Zone 3 opens up discussions on optimal zone selection and deployment strategies within Azure&rsquo;s Western Europe region. Is this the reason that Zone 3 is the least populated zone? Online there isn’t much to find about this it looks like. So i wrote this post since this cost us literally days to pinpoint this performance difference.</p>
<blockquote>
<p>This will impact our production environment. We are going to run our DB in Zone 1 or 2. and the API must reside in either of these zones. Zone 3 can be used for pods that talk with the API.</p>
</blockquote>
<p>This latency anomaly in Zone 3 was more then a challenge for us. We took it as a opportunity to learn, adapt, and optimize our knowledge. As we continue to collaborate, test, and refine our approach, our journey into Azure is shaping up to be a very interesting technical exploration which is broadening our toolset for the future.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hotfixing with Docker Artifacts in a Git Flow Strategy</title>
      <link>https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/</link>
      <guid isPermaLink="false">65d330e818945800013cc243</guid>
      <pubDate>Tue, 20 Feb 2024 09:53:55 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Teams]]></category><category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Master hotfixes in Kubernetes with Docker. Learn to rapidly deploy critical fixes in a CI/CD pipeline focusing on streamlined branching. Perfect for tackling urgent issues without disrupting flow. Dive in now.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>This post is a follow up on GIT deployment strategies within Kubernetes environments. Make sure you read this post first:</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/"><span class="bookmark__body"><span class="bookmark__title">CI/CD pipeline with Docker and Git branching strategy</span><span class="bookmark__description">Explore advanced CI/CD with Docker &amp; Git. Master deploying Docker artifacts via Git branching. Dive into strategies for DEV, STAGING, LIVE environments. Perfect your pipeline now!</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/bookmarks/01-thumb_hu_4b0a9042ec0e16fc.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<p>In a CI/CD pipeline centered around Docker artifacts with the focus on a singular branch, implementing an efficient hotfix strategy requires a small deviation to ensure rapid deployment of critical fixes. Let&rsquo;s work out our approach to hotfixing in this Docker-centric environment, given the explained branching architecture in the above post.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/images/Git-branches-and-docker-environments-ci_cd-1_hu_17e1895a357c0fa7.webp" alt="" width="560" height="378" data-zoom-src="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/images/Git-branches-and-docker-environments-ci_cd-1_hu_7bd9f2a51f818afa.webp" loading="lazy" decoding="async">
  </picture><figcaption>Graphical reference of our branching and deployment strategy</figcaption></figure>

<h2 id="identifying-the-need-for-a-hotfix">Identifying the need for a Hotfix</h2>
<p>Hotfixes are a critical part of maintaining the stability and reliability of any software in production. They are typically required under specific circumstances that demand immediate attention. Understanding when to implement a hotfix is key to managing your CI/CD pipeline effectively, especially when using Docker artifacts and streamlined branching strategies like focusing primarily on a Single branch. Here are our scenarios where a hotfix can be necessary:</p>
<ol>
<li><strong>Security Vulnerabilities:</strong> One of the most urgent triggers for a hotfix is the discovery of a security vulnerability within your application. Whether it&rsquo;s an exposed data risk, a potential for unauthorized access, or any flaw that could be exploited maliciously, quick action is required to patch the vulnerability and protect your users and data.</li>
<li><strong>Critical Bugs Impacting User Experience:</strong> If a significant bug slips through your testing processes and reaches production, it can severely impact the user experience. This could be anything from a broken core functionality that prevents users from using your application as intended to severe performance issues leading to timeouts or crashes. If the issue significantly affects your user base, a hotfix is warranted.</li>
<li><strong>Compliance Issues:</strong> Certain applications may have legal or regulatory requirements. If a compliance issue is identified that could affect your legal standing or user privacy, addressing it swiftly through a hotfix becomes a priority to avoid penalties or breaches of trust.</li>
<li><strong>Data Loss Risks:</strong> Scenarios where there&rsquo;s a risk of data loss or corruption for users require immediate attention. Whether it&rsquo;s a flaw in how data is saved, updated, or retrieved, a hotfix should be deployed to mitigate any potential loss or corruption of user data.</li>
<li><strong>Payment or Transaction Failures:</strong> For applications dealing with payments or any form of transactions, issues that prevent users from completing transactions or that might lead to financial discrepancies are critical. Such problems not only affect user trust but can also have financial repercussions for both users and the service provider, necessitating a hotfix.</li>
</ol>
<h2 id="deploying-a-hotfix">Deploying a Hotfix</h2>
<p>When a critical issue is identified within the production environment, the immediate goal is to develop, test, and deploy a hotfix as quickly as possible. Given that our primary branch for development is MAIN and that we generate Docker artifacts for deployment, we need a process that allows for rapid intervention without disrupting the ongoing work.</p>
<ol>
<li><strong>Creating a Hotfix Branch</strong>: Instead of branching off from your Production branch (as LIVE is not represented anymore in the Git repository in this strategy), the hotfix branch is created from the commit currently deployed in production. This ensures that the Hotfix is based on the exact state of the version running on the production environment. The specific commit can be identified through tags or Docker image labels that correlate with the production deployment.</li>
<li><strong>Developing and Testing the Hotfix</strong>: The critical fix is developed within this hotfix branch. The urgency of the situation necessitates a focused approach, targeting the problem with minimal changes to expedite the testing and deployment process. Despite the rush, adherence to the Definition of Done (DoD) is crucial to ensure the stability and reliability of the hotfix. This can mean a condensed version of the DoD, prioritizing critical tests and checks that ensure the fix will not introduce new issues.</li>
<li><strong>Creating a Docker Artifact for the Hotfix</strong>: Once the hotfix meets the agreed DoD criteria, an automated process builds a Docker image from the hotfix branch. This Docker artifact is then tagged appropriately, indicating its purpose and urgency.</li>
<li><strong>Deploying the Hotfix</strong>: The newly created Docker artifact is deployed directly into the production environment, bypassing the usual DEV to STAGING to PRODUCTION environment strategy. This deployment is only justified by the critical nature of the fix and the testing it has undergone.</li>
<li><strong>Merging Back and Maintaining Consistency</strong>: After the hotfix is successfully running in production, it&rsquo;s essential to merge the changes back into the MAIN branch and any other relevant branches or tags. Only this step ensures that the fix is incorporated into the ongoing development work and future releases, preventing regression!!</li>
</ol>
<h2 id="ensuring-rapid-deployment-and-minimizing-disruption">Ensuring Rapid Deployment and Minimizing Disruption</h2>
<ul>
<li><strong>Automated Processes</strong>: Automation plays a key role in this strategy, from the creation of the Docker artifact to its deployment. Automated tests, builds, and deployments ensure that the hotfix process is as fast and efficient as possible.</li>
<li><strong>Communication and Monitoring</strong>: Rapid deployment of hotfixes requires clear communication among team members and close monitoring of the production environment post-deployment to ensure the fix is effective and does not introduce new issues.</li>
<li><strong>Documentation and Review</strong>: Documenting the hotfix process and conducting post-mortem reviews help in refining the hotfix strategy, ensuring that the team can respond even more effectively to future issues.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>In a CI/CD pipeline using Docker and primarily operating through Git Flow, hotfixing requires a streamlined approach that leverages automation and focuses on rapid, targeted fixes. By creating hotfix branches based on the production-deployed commit, developing and testing fixes according to an well defined DoD, and deploying Docker artifacts directly into production, teams can address critical issues swiftly while ensuring the ongoing stability and integrity of their application.</p>
]]></content:encoded>
    </item>
    <item>
      <title>CI/CD pipeline with Docker and Git branching strategy</title>
      <link>https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/</link>
      <guid isPermaLink="false">65d3035a18945800013cc216</guid>
      <pubDate>Tue, 20 Feb 2024 09:50:55 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Teams]]></category><category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Exploring CI/CD with Docker &amp; Git. Mastering deploying Docker artifacts via Git branching. Dive into strategies for DEV, STAGING, PROD environments. Perfect your pipeline!]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<p>Let&rsquo;s dive into managing a CI/CD pipeline using Docker with a focus on Git branching strategy that involves development (DEV), staging (STAGING), and production (LIVE) branches. Our goal is to clarify the role each branch plays in the process, particularly how Docker artifacts are created and managed within this framework.</p>
<h2 id="docker-deployment-using-git-flow-with-multiple-branches">Docker deployment using Git Flow with multiple branches</h2>
<p>The core of our branching strategy involved three branches:</p>
<ol>
<li><strong>DEV</strong>: This is where all development work happens. Every new feature, bug fix, or enhancement starts in a feature branch, branched off from DEV. Once the work on a feature branch is complete, reviewed and meets the Definition of Done (DoD), it&rsquo;s merged back into DEV.</li>
<li><strong>STAGING</strong>: This branch mirrors a pre-production environment. It&rsquo;s where integrated features from DEV get tested as a whole and accepted by stakeholders to ensure they work together seamlessly.</li>
<li><strong>MASTER</strong>: The branch that reflects the current state of our live environment. Only thoroughly tested and stakeholder-approved changes from STAGING make it to Production.</li>
</ol>
<figure class="bookmark"><a class="bookmark__link" href="https://dev.to/preethamsathyamurthy/git-branching-and-branching-strategy-4mci" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Git Branching and Branching Strategy</span><span class="bookmark__description">In this post, we will go over why branching is required, the difference between development, staging…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/01-icon_hu_4e78872e34382e92.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>DEV Community</span><span>Preetham</span></span></span></a></figure>

<p>This was our variation of a Git Flow strategy, we used until now. When you start working with Artifacts this all changes back to a normal Git Flow structure. We still have environments running (Dev, Staging and Prod). The real difference is that code commits get done once instead of getting merged into a branch running on Staging or in Production.</p>
<p>Instead we now have one Branch we called it Main. This is where a runnable artifact gets created after a successful code merge using Docker images. Everything deployed into our Staging or Production environment is a specific version of an earlier created Artifact.</p>
<p>The biggest change is that STAGING and PROD aren’t branches anymore where you merge code into. They have instead became environments that run a specific version of a Docker image. This all has its own challenges, especially when you need to Hotfix. Hotfixing has it’s own post which you can read after this post. Lets break down all of this without introducing hotfixing step by step first. It is comprehensive enough already.</p>
<h2 id="what-is-an-artifact">What is an Artifact?</h2>
<p>A Docker artifact, often referred to interchangeably with a Docker image, is a lightweight, standalone, executable package that includes everything needed to run a piece of software, including the code, a runtime, libraries, environment variables, and config files. In simple terms, it&rsquo;s like a snapshot of your application and its environment, bundled together, ready to be deployed and run anywhere Docker is installed, ensuring consistency across different environments.</p>
<h2 id="the-importance-of-dod-when-merging-code">The Importance of DoD When Merging code</h2>
<p>A comprehensive and clear Definition of Done (DoD) plays a pivotal role in this new process, even more when merging changes into a single branch that triggers a Docker image which should be runnable and deployable:</p>
<ul>
<li><strong>Quality Assurance</strong>: The DoD ensures that all changes, including new features, enhancements, and hotfixes merged back into MAIN, meet a consistent quality standard. This includes code quality checks, successful completion of automated tests, and adherence to security best practices.</li>
<li><strong>Preventing Regression</strong>: By applying the DoD to all merges into MAIN, the team can prevent regressions and ensure that new changes do not negatively affect existing functionality. This is crucial for maintaining the integrity and stability of the codebase.</li>
<li><strong>Streamlining Deployments</strong>: A clear and comprehensive DoD facilitates smoother deployments by ensuring that only fully vetted and ready-to-deploy changes are merged into MAIN. This reduces the risk of wasting resources on building Docker images that are bugged or broken, and therefore contributes to more efficient and reliable artifacts.</li>
</ul>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/images/Git-branches-and-docker-environments-ci_cd_hu_17e1895a357c0fa7.webp" alt="" width="560" height="378" data-zoom-src="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/images/Git-branches-and-docker-environments-ci_cd_hu_7bd9f2a51f818afa.webp" loading="lazy" decoding="async">
  </picture><figcaption>Here a graphical presentation of our Git Flow strategy and how this flows trough the different environments we have.</figcaption></figure>

<h2 id="creating-and-managing-artifacts">Creating and Managing Artifacts</h2>
<p>The first thing for this proces to work is when a forked branch is merged back into MAIN, this triggers an automated process in our CI/CD pipeline. Here&rsquo;s what happens:</p>
<ol>
<li><strong>Automated Testing</strong>: First, automated tests run to ensure the new changes meet our quality standards. This is crucial for maintaining a stable branch.</li>
<li><strong>Docker Artifact Creation</strong>: Upon successful running al tests, a Docker image (our Artifact) is created from the MAIN branch. This is because the MAIN branch is the single branch that represents the latest state of every DONE development, This is where we generate our artifacts from that can become “shippable” products.</li>
<li><strong>Why using MAIN for Docker Artifacts?</strong>: Creating artifacts at this stage allows us to catch any integration issue. Since every feature merged into MAIN is going to be considered ready for a release, generating an artifact here ensures that we always have a tested and potentially deployable version of our product at hand.</li>
</ol>
<h2 id="transferring-from-branches-to-environments">Transferring from Branches to Environments</h2>
<p>In traditional Git workflows, branches like STAGING and PROD are used to represent different stages of the development process, from testing to production. However, in a Docker-centric CI/CD pipeline, the focus shifts from using these branches for code integration to using them as references for deployment environments.</p>
<h2 id="deploying-to-staging">Deploying to STAGING</h2>
<ul>
<li><strong>Creating Docker Artifacts</strong>: Once development is complete and merged into the MAIN branch, a Docker artifact is automatically created. This artifact is a snapshot of your application at that point in time, encapsulating all the changes that were merged.</li>
<li><strong>Deployment, Not Merging</strong>: The STAGING environment&rsquo;s role is to test this Docker artifact in conditions that closely mimic the production environment. Instead of merging code from MAIN to a STAGING branch, you deploy the Docker artifact to a STAGING environment. This means the code itself isn&rsquo;t moving between branches; rather, the compiled and containerized version of your application is being tested.</li>
<li><strong>Purpose of STAGING</strong>: The STAGING environment is essential for comprehensive testing, including performance and security assessments. It&rsquo;s a final check to ensure everything works as expected in a production-like setting without directly affecting your actual users.</li>
</ul>
<h2 id="going-to-production">Going to Production</h2>
<ul>
<li><strong>Stakeholder Approval</strong>: Once your Docker artifact has been rigorously tested in STAGING and approved by stakeholders, it&rsquo;s ready to move to production. This step is crucial for ensuring that all features, fixes, and updates meet the necessary standards and expectations.</li>
<li><strong>Deployment to Production</strong>: The transition from STAGING to PROD involves deploying the tested Docker artifact to your production environment. This deployment process ensures consistency, as the exact artifact tested in STAGING is what gets deployed to PROD, eliminating any discrepancies between tested and deployed code.</li>
</ul>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li><strong>No More Branch Merging for Deployment</strong>: In this streamlined workflow, STAGING and PROD branches are no longer used for merging code. Instead, they reference specific deployment environments where Docker artifacts are deployed for testing and production.</li>
<li><strong>Focus on Artifacts</strong>: The emphasis is on creating, testing, and deploying Docker artifacts. These artifacts represent stable versions of your application ready for STAGING and, eventually, PROD deployment.</li>
<li><strong>Consistency and Reliability</strong>: By deploying the same Docker artifact from STAGING to PROD, you ensure that what has been tested is exactly what gets released, increasing the reliability and predictability of your deployments.</li>
</ul>
<p>This approach simplifies the deployment process, reduces the risk of errors, and ensures that every release is thoroughly tested in an environment that mirrors production as closely as possible.</p>
<h2 id="are-multiple-branches-still-needed">Are multiple Branches still needed?</h2>
<p>Given this new workflow, the STAGING and PROD branches in Git branching serve no active purpose anymore and therefore they have became obsolete. They helped track which versions of the code have been deployed to each environment which we have solved by running a specific version of an Artifact.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Incorporating Docker into our Git branching strategy streamlines the development-to-deployment process by ensuring that every merge into MAIN triggers a Docker image with the potential to go to production. The key to this strategy is the creation of Docker artifacts early in the development cycle, allowing for continuous integration and delivery while maintaining high standards of quality and stability.</p>
<p>By understanding the changes to Git branches and the process of creating and deploying Docker artifacts, we can rely on the complexities of CI/CD with confidence, ensuring that all new features and fixes are delivered to users as efficiently and reliably as possible.</p>
<h2 id="hotfixing">Hotfixing</h2>
<p>We havent spoken about hotfixing yet. As i said this deserves its own Post. You can read all about Hotfixing and its caveats here.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/hotfixing-with-docker-artifacts-in-a-single-branch-strategy/"><span class="bookmark__body"><span class="bookmark__title">Hotfixing with Docker Artifacts in a Single Branch Strategy</span><span class="bookmark__description">Master hotfixes in Kubernetes with Docker. Learn to rapidly deploy critical fixes in a CI/CD pipeline focusing on streamlined branching. Perfect for tackling urgent issues without disrupting flow. Dive in now.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/02-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/ci-cd-pipeline-with-docker-and-git-branching-strategy/bookmarks/02-thumb_hu_31f1067c4e9c7ce6.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
    <item>
      <title>Issues displaying images from our Static server</title>
      <link>https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/</link>
      <guid isPermaLink="false">6551ee1b3a3fb5000134167b</guid>
      <pubDate>Sun, 12 Nov 2023 12:50:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[We noticed that in the browsers we use (Edge and Chrome) there were reports from the failed images in the console which reported. ERR_HTTP2_PROTOCOL_ERROR]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="some-background-to-understand-our-issue-better">Some background to understand our issue better</h2>
<p>We are working on a new website with a great emphasis on images. We already have <a href="http://www.333travel.nl/" data-external>www.333travel.nl</a> which of course being a travel product already heavily relies on images. Our new website has even higher res images loaded because there is a great emphasis on the luxury feel. This site is aimed towards the luxury traveller <a href="http://www.travelplatinum.nl/" data-external>www.travelplatinum.nl</a>. The project that served our images static.333travel.nl is already a few years running with no reported issues for the 333travel website.</p>
<h2 id="the-issue">The issue</h2>
<p>The issue at hand got reported from our home workers. They noted that when working from home a lot of the images displayed were sometimes broken on TravelPlatinum (and a refresh mostly fixed them). They also replied that it was all working fine when working from within the office. So that made us start a quest in finding the culprit.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-8526b527-c281-496a-9251-1028b11d3560_hu_1bb3200cf0522d93.webp" alt="" width="560" height="382" data-zoom-src="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-8526b527-c281-496a-9251-1028b11d3560_hu_220750cfd07c6574.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>We noticed that in the browsers we use (Edge and Chrome) there were reports from the failed images in the console which reported. ERR_HTTP2_PROTOCOL_ERROR. With that message in the pocket we started our search for the issue searching through the logs on the server. We are running Nginx as our webserver.</p>
<p>We quickly found a trail that appeared for every image that we thought didn’t load correctly. Since we weren’t able to simulate the issue ourselves yet.</p>
<p>0.0.0.0 - - [06/Nov/2023:15:45:02 +0100] &ldquo;GET /web-images/10/2023/653775730fc66/Jazan&mdash;Al-qahar-mountains-1200.webp HTTP/2.0&rdquo; 206 183064 &ldquo;<a href="https://www.travelplatinum.nl/" data-external>https://www.travelplatinum.nl/</a>&rdquo; &ldquo;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0&rdquo;</p>
<p>Pay attention to the 206 call here. This means partial content loaded.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206" data-external>https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206</a></p>
<p>This was odd since our webserver didn’t support partial loading of images. We never send headers that replied with something like a range which are required to be able to do partial content.</p>
<h2 id="our-attempt-to-fix-the-issue">Our attempt to fix the issue</h2>
<p>So that was the first issue we dove in. I found out that Nginx by default sends a header that replies Content-Type: bytes. I don’t know why I would want our image server to respond to a 206 request with an ok message. So we disabled this with the following setting in our server block.</p>
<p>server {<br>
server_name static.333travel.nl;<br>
max_ranges 0;<br>
}</p>
<p>We replied back to the persons experiencing this issue to report to us if it happened again. Which sadly was quickly the case. The error message did change in our logging. Edge and chrome kept spitting out the same useless ERR_HTTP2_PROTOCOL_ERROR in the console tab. In the timeline we also saw these images stopping to load at a certain point.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-1cdc5aec-ad55-4bfd-8a7c-2593b57282be_hu_597e0975f3b81981.webp" alt="" width="560" height="247" data-zoom-src="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/images/data-src-image-1cdc5aec-ad55-4bfd-8a7c-2593b57282be_hu_ccaba7f5f6ea432e.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>0.0.0.0 - - [08/Nov/2023:14:22:27 +0100] &ldquo;GET /web-images/06/2020/5ed7ad48d3d79/mexico-yucatan-chichen-itza-ik-kil-cenote.jpeg HTTP/2.0&rdquo; 200 0 &ldquo;<a href="https://vue-boss.333office.nl/" data-external>https://vue-boss.333office.nl/</a>&rdquo; &ldquo;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0&rdquo;</p>
<p>We now get a header with a length of 0 bytes and a response with http 200 (ok). This is getting real strange here. The header when inspected in the browser returns the right amount of bytes in the response header. So why does the log of Nginx state it replies with a size of 0. This is where things got weird and we didn’t really know what to do next. We first had to be able to reproduce the issue ourself so we could really start to troubleshoot.</p>
<p>I tried to simulate a fault with ab (apache benchmark). I started conservative with 500 requests and 50 concurrent requests. The issue never triggered for me. i then moved in small steps upwards to end on 10.000 requests with 700 concurrent requests without ever triggering the reported issue. The server load also appears to handle everything fine. It was an image of around 500kb i requested there.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/images/image_hu_e4a7c89173cb9060.webp" alt="" width="560" height="339" data-zoom-src="https://www.allsubjectsmatter.nl/sudden-issues-loading-images-from-our-static-project/images/image_hu_52a1f89bab47586c.webp" loading="lazy" decoding="async">
  </picture></figure>

<h2 id="the-facts">The Facts</h2>
<ol>
<li>Setting a slow profile in the network tab when inspecting didn’t simulate the issue for us.</li>
<li>The error was still only reported working from home from 2 users.</li>
<li>When in the office they haven’t had a single issue.</li>
<li>Loading the site 333travel.nl didn’t report the issue at all. Every image worked there also for the 2 persons working from home.</li>
<li>Using a 5g connection testing from all various places we could think off didn’t simulate the issue once for me.</li>
<li>Using ab (apache benchmark) doing 10.000 requests with 700 concurrent requests never triggered the issue at hand.</li>
</ol>
<p>There was still one difference and that was the domain serving the images. 333travel got replied with a Cross origin header of same_site which of course is true since the domain is the same. So I added a header to the static.</p>
<p>server {<br>
server_name static.333travel.nl;<br>
max_ranges 0;<br>
add_header &lsquo;Access-Control-Allow-Origin&rsquo; &lsquo;*&rsquo;;<br>
}</p>
<p>This way every source wanting to use images from our static.333travel.nl gets the same reply. This didn’t solve anything about the issue reported.</p>
<p>We still experienced issues to simulate the issue ourself. We were pretty sure that this only got triggered on a somehow bad connection visiting TravelPlatinum. Visiting 333travel which served the images from the same static server never showed the issue we are experiencing on TravelPlatinum.</p>
<h2 id="simulating-a-bad-wifi-connection">Simulating a bad WiFi connection</h2>
<p>We badly wanted to simulate a bad WiFi connection. I moved myself to a spot where there was bad WiFi outside of our office and I started to finally see the issue for myself. Finally something to start our testing on. While it was still odd we needed a somehow bad/slow WiFi to simulate this. You would say setting your profile to slow 3g in the browsers developer tools would behave the same.</p>
<p>Now we finally were able to start testing from our development environment. We started to suspect the project itself doing something weird with maybe something javascript related closing the connection or something. So we started to create a stripped down website from TravelPlatinum to test without javascript.</p>
<p>The no image loading issue was still there..</p>
<p>We decided to take in the exact same html from 333travel.nl to test with that to create a same environment. Since 333travel still was going strong every refresh.</p>
<p>The no image loading issue was still there..</p>
<p>We badly wanted to be able simulate this even better since our bad WiFi spot wasn’t at a desk or something, we were sitting on a bunch of stairs debugging the issue from there. I decided to create a 2<sup>nd</sup> WiFi hotspot in our office with a throttled connection of 3mbit and see where that takes us. This was it! We could now simulate every refresh a lot of broken images on TravelPlatinum! We are getting somewhere at least. Bear in mind that the 333travel website still was loading every image with no issues.</p>
<p>We moved away from the nginx logs at this moment in time since we uncovered that going in that direction nothing was going to be solved at this moment. Nginx still replied an ok on a header with 0 bytes. If that’s ok for nginx with the right headers in place, that message is not going to help us solve the issue at this moment.</p>
<h2 id="testing-with-firefox">Testing with Firefox</h2>
<p>The moment I created this WiFi hotspot we were just about ready to test images with just loading the html image tag and nothing more loaded in the project. This still broke our images. I then suggested to fire up Firefox just to see if that behaved differently since I suspected it to be a browser related thing on how it responds on what is happening. Or at least it could output maybe a different error. Something we could focus our search for the solution on.</p>
<p>We saw that Firefox was indeed loading the images line by line on the 3mbit connection. But it suddenly stopped loading leaving broken images in place which were about 50% loaded or not at all. This was finally something of a change to see! Still Edge/Chrome displayed no image at all and gave me a useless error message. I now finally started to realize that the static was actually sending the images and the browser received them but the connection got closed to soon somehow.</p>
<h2 id="the-solution">The solution</h2>
<p>And that was when I finally found the issue at hand. A header i already set ages ago on a global level in Nginx in a performance tuning session.</p>
<p>send_timeout 2;</p>
<p>What send timeout in my opinion does is keeping the connection open for X seconds after not receiving data. The default is 60 seconds and I adjusted it to 2 seconds so we would not have open connections for a whole 60 seconds doing nothing.</p>
<p>Why would that be the issue since I clearly saw Firefox visually loading the image line by line? There is constant data being send then right? Why would this setting then be related to closing the connection.</p>
<table>
	<thead>
			<tr>
					<th>Syntax:</th>
					<th><strong>send_timeout</strong> <em>time</em>;</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Default:</td>
					<td>send_timeout 60s;</td>
			</tr>
			<tr>
					<td>Context:</td>
					<td>http, server, location</td>
			</tr>
	</tbody>
</table>
<p><em>Sets a timeout for transmitting a response to the client. The timeout is set only between two successive write operations, not for the transmission of the whole response. If the client does not receive anything within this time, the connection is closed.</em></p>
<p>Well it appears this does get triggered on slower connections. This is getting interpreted as a stalled request by Nginx. And it closes the whole connection to in our case the static.333travel.nl. So the partially loaded image and every image after that not being loaded wont get loaded again since the connection to static is closed.</p>
<p>Why simulating a slow connection in the developer tools wont trigger this issue is still something I don’t know for sure. I have some theories, a bad WiFi also introduces more latency and lost packets getting resend. But I don’t know the real reason. I dont know how these browsers simulate a slow connection.</p>
<p>Setting this send_timeout to 8 seconds as below solved our issue completely.</p>
<p>server {<br>
server_name static.333travel.nl;<br>
max_ranges 0;<br>
add_header &lsquo;Access-Control-Allow-Origin&rsquo; &lsquo;*&rsquo;;<br>
send_timeout 8;<br>
}</p>
<h2 id="conclusion">Conclusion</h2>
<p>We have learned a lot, and we solved the issue. The odd thing remains that all this time it only triggered on our TravelPlatinum project. We were obviously balancing on the limit with the send_timeout setting since the image load is greater on TravelPlatinum.</p>
<p>Another important thing to note, setting a slow connection in your developer tools is not a real simulation from a slow connection. You can simulate all you want but you need real life cases to really measure if everything is working fine.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Useful commands</title>
      <link>https://www.allsubjectsmatter.nl/useful-commands/</link>
      <guid isPermaLink="false">66e14258aea2140001bf043f</guid>
      <pubDate>Mon, 11 Sep 2023 09:21:00 +0200</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Useful commands and scripts that simplify technical tasks, like managing logs using Notepad&#43;&#43; with regex or automating DNS checks for domains.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/useful-commands/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="notepad-regexp">Notepad++ regexp</h2>
<p>For example checking your php-fpm access/slow log file. Use mark in notepad++ to mark all lines that are slower then x/y.</p>
<pre class="line-numbers language-text"><code class="language-text">10000ms en trager 

^.*\s(\d{5,}\.\d{3}ms).*

5000ms en trager 

^.*\s(5\d{3}\.\d{2,}ms|\d{5,}\.\d{2,}ms).*</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="dns-tools-to-automatically-check-the-transfer-of-your-dns">DNS tools to automatically check the transfer of your DNS</h2>
<pre class="line-numbers language-bash"><code class="language-bash">sudo apt update
sudo apt install dnsutil

nano check_nameservers.sh</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">#!/bin/bash

DOMAIN=&amp;#34;jouwdomein.com&amp;#34;
EXPECTED_NS1=&amp;#34;ns1.cloudflare.com.&amp;#34;
EXPECTED_NS2=&amp;#34;ns2.cloudflare.com.&amp;#34;

# Get the current nameservers
CURRENT_NS=$(dig NS $DOMAIN &#43;short)

# Check if the current nameservers match the expected ones
if [[ $CURRENT_NS == *&amp;#34;$EXPECTED_NS1&amp;#34;* &amp;amp;&amp;amp; $CURRENT_NS == *&amp;#34;$EXPECTED_NS2&amp;#34;* ]]; then
    echo &amp;#34;Nameservers are correctly set to Cloudflare.&amp;#34;
else
    echo &amp;#34;Nameservers have not yet switched to Cloudflare.&amp;#34;
fi</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-bash"><code class="language-bash">chmod &#43;x check_nameservers.sh

watch -n 1 ./check_nameservers.sh</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre>]]></content:encoded>
    </item>
    <item>
      <title>Docker Nginx HTTP/3 and Brotli</title>
      <link>https://www.allsubjectsmatter.nl/docker-nginx-http3-and-brotli/</link>
      <guid isPermaLink="false">64183271dba1d1003d7c539e</guid>
      <pubDate>Mon, 20 Mar 2023 11:20:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[Here you will find a working docker file i created as a base to build my projects on. This a recent Nginx with compiled HTTP/3 support and Brotli support for better compression. This is posted here to share it back with anyone.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/docker-nginx-http3-and-brotli/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="update-13-11-2024">Update (13-11-2024)</h2>
<p>Somehow i lost the creating of a new image to copy all the builder items from. It changed nothing about the way the image worked but the image size is much smaller now. It is under 100mb from more then 2gb. Somewhere in my previous debugging we lost this step. So the lastest update can be found below again.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/"><span class="bookmark__body"><span class="bookmark__title">Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe</span><span class="bookmark__description">Unveiling a significant Azure performance issue: Zone 3’s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/docker-nginx-http3-and-brotli/bookmarks/01-icon_hu_9b466dd874965fcb.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>All Subjects Matter</span><span>Remco Loup.</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/docker-nginx-http3-and-brotli/bookmarks/01-thumb_hu_27e6839ff9d3a962.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<h2 id="introduction">Introduction</h2>
<p>In here you will find a working Dockerfile I created with the below defined compatibility needs for myself. This is posted here for my own archive and to share of course. I added the required support for the following.</p>
<ul>
<li>HTTP/2 HPACK</li>
<li>HTTP/3</li>
<li>Boringssl</li>
<li>Brotli support</li>
</ul>
<p>This version of Nginx supports HTTP/3 quic and Brotli compression as a main benefit with some other fixes. Not having Brotli support these days is just a step back in the delorean.</p>
<p>More info about how <a href="https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/" data-external>HPACK</a> accelerates your HTTP/2. And here you can read more details about <a href="https://blog.ononoki.org/enable-http3-for-nginx/" data-external>BoringSSL</a></p>
<h2 id="brotli-compared-to-gzip">Brotli compared to GZIP</h2>
<p>As mentioned this is a Nginx version which supports Brotli. Brotli is very useful as it is a better compression algorithm then GZIP. If you are still not convinced watch the video below.</p>
<figure class="embed"><iframe src="https://www.youtube-nocookie.com/embed/BMCvMWqoivs" title="How File Compression Increases Your Website Speed: BROTLI vs GZIP Algorithms" loading="lazy" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></figure>

<h2 id="nginx-base-docker-file">Nginx base Docker file</h2>
<p>This is the base of my dockerfile which takes care of all the things mentioned above. You can of course edit this or use different versions of alpine or Nginx regarding this wont break building the other modules. I also updated Nginx, NJS and BoringSSL to the latest versions.</p>
<p>I use this image instead of the official Nginx to load all my web projects. See this as a base image to define all your running webserver projects in. Only you get &ldquo;free&rdquo; Brotli compression and HTTP/3 support with it so you can make your sites faster and more up to date.</p>
<pre class="line-numbers language-docker"><code class="language-docker">##################################################
# Nginx with Quiche (HTTP/3) and Brotli
# VERSION 1.05 / 13-11-2024
##################################################
# This build compiles Nginx with Brotli and
# HTTP/3 support. It uses a multi-stage build to
# minimize the final image size by copying only
# the necessary artifacts from the builder image.
# Based on Alpine for a lean and efficient image.
##################################################

# Builder Stage
FROM alpine:latest AS builder

# Set the versions of Nginx, NJS, and BoringSSL
ENV NGINX_VERSION=1.27.2
ENV NJS_VERSION=0.8.7
ENV BORINGSSL_VERSION=c59bf8bf189dcbde868e04efcd53b705ed155231
ENV BORINGSSL=&amp;#34;/tmp/boring-nginx&amp;#34;

# Build-time metadata
ARG BUILD_DATE
ARG VCS_REF

# Install dependencies for building Nginx
RUN addgroup -S nginx \
  &amp;amp;&amp;amp; adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \
  &amp;amp;&amp;amp; apk update \
  &amp;amp;&amp;amp; apk upgrade \
  &amp;amp;&amp;amp; apk add --no-cache ca-certificates openssl \
  &amp;amp;&amp;amp; update-ca-certificates \
  &amp;amp;&amp;amp; apk add --no-cache --virtual .build-deps \
      gcc libc-dev make pcre-dev zlib-dev linux-headers gnupg \
      libxslt-dev gd-dev geoip-dev perl-dev \
  &amp;amp;&amp;amp; apk add --no-cache --virtual .brotli-build-deps \
      autoconf libtool automake git g&#43;&#43; cmake go perl rust cargo patch \
      libxml2-dev byacc flex libstdc&#43;&#43; libmaxminddb-dev lmdb-dev file openrc

# Install and build BoringSSL
RUN mkdir $BORINGSSL \
  &amp;amp;&amp;amp; cd $BORINGSSL \
  &amp;amp;&amp;amp; git clone https://boringssl.googlesource.com/boringssl \
  &amp;amp;&amp;amp; cd boringssl \
  &amp;amp;&amp;amp; git checkout -q ${BORINGSSL_VERSION} \
  &amp;amp;&amp;amp; mkdir build \
  &amp;amp;&amp;amp; cd build \
  &amp;amp;&amp;amp; cmake -DBUILD_SHARED_LIBS=1 .. \
  &amp;amp;&amp;amp; make

# Prepare BoringSSL for Nginx compilation
RUN mkdir -p &amp;#34;$BORINGSSL/boringssl/.openssl/lib&amp;#34; \
  &amp;amp;&amp;amp; cd &amp;#34;$BORINGSSL/boringssl/.openssl&amp;#34; \
  &amp;amp;&amp;amp; ln -s ../include include \
  &amp;amp;&amp;amp; cd &amp;#34;$BORINGSSL/boringssl&amp;#34; \
  &amp;amp;&amp;amp; cp &amp;#34;build/crypto/libcrypto.so&amp;#34; &amp;#34;.openssl/lib&amp;#34; \
  &amp;amp;&amp;amp; cp &amp;#34;build/ssl/libssl.so&amp;#34; &amp;#34;.openssl/lib&amp;#34; \
  &amp;amp;&amp;amp; cp &amp;#34;.openssl/lib/libssl.so&amp;#34; /usr/lib/ \
  &amp;amp;&amp;amp; cp &amp;#34;.openssl/lib/libcrypto.so&amp;#34; /usr/lib/

# Clone Nginx modules
RUN mkdir /usr/src \
  &amp;amp;&amp;amp; cd /usr/src \
  &amp;amp;&amp;amp; git clone --depth=1 --recursive --shallow-submodules https://github.com/google/ngx_brotli \
  &amp;amp;&amp;amp; git clone --branch $NJS_VERSION --depth=1 --recursive --shallow-submodules https://github.com/nginx/njs

# Build Nginx with modules
RUN cd /usr/src \
  &amp;amp;&amp;amp; wget -qO nginx.tar.gz https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz \
  &amp;amp;&amp;amp; tar -zxC /usr/src -f nginx.tar.gz \
  &amp;amp;&amp;amp; rm nginx.tar.gz \
  &amp;amp;&amp;amp; cd /usr/src/nginx-$NGINX_VERSION \
  &amp;amp;&amp;amp; mkdir /root/.cargo \
  &amp;amp;&amp;amp; echo $&amp;#39;[net]\ngit-fetch-with-cli = true&amp;#39; &amp;gt; /root/.cargo/config.toml \
  &amp;amp;&amp;amp; ./configure --prefix=/etc/nginx \
      --sbin-path=/usr/sbin/nginx \
      --modules-path=/usr/lib/nginx/modules \
      --conf-path=/etc/nginx/nginx.conf \
      --error-log-path=/var/log/nginx/error.log \
      --http-log-path=/var/log/nginx/access.log \
      --pid-path=/var/run/nginx.pid \
      --lock-path=/var/run/nginx.lock \
      --http-client-body-temp-path=/var/cache/nginx/client_temp \
      --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
      --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
      --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
      --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
      --user=nginx \
      --group=nginx \
      --with-pcre-jit \
      --with-http_ssl_module \
      --with-http_realip_module \
      --with-http_addition_module \
      --with-http_sub_module \
      --with-http_dav_module \
      --with-http_flv_module \
      --with-http_mp4_module \
      --with-http_gunzip_module \
      --with-http_gzip_static_module \
      --with-http_random_index_module \
      --with-http_secure_link_module \
      --with-http_stub_status_module \
      --with-http_auth_request_module \
      --with-http_xslt_module=dynamic \
      --with-http_image_filter_module=dynamic \
      --with-http_geoip_module=dynamic \
      --with-http_perl_module=dynamic \
      --with-threads \
      --with-stream \
      --with-stream_ssl_module \
      --with-stream_ssl_preread_module \
      --with-stream_realip_module \
      --with-stream_geoip_module=dynamic \
      --with-http_slice_module \
      --with-mail \
      --with-mail_ssl_module \
      --with-compat \
      --with-file-aio \
      --with-http_v2_module \
      --with-http_v3_module \
      --add-module=/usr/src/ngx_brotli \
      --add-module=/usr/src/njs/nginx \
      --with-cc-opt=&amp;#34;-I$BORINGSSL/boringssl/include -Wno-error&amp;#34; \
      --with-ld-opt=&amp;#34;-L$BORINGSSL/boringssl/build/ssl -L$BORINGSSL/boringssl/build/crypto&amp;#34; \
      --with-select_module \
      --with-poll_module \
      --build=&amp;#34;docker-nginx-http3-$VCS_REF-$BUILD_DATE ngx_brotli-$(git --git-dir=/usr/src/ngx_brotli/.git rev-parse --short HEAD) njs-$(git --git-dir=/usr/src/njs/.git rev-parse --short HEAD)&amp;#34; \
  &amp;amp;&amp;amp; touch &amp;#34;$BORINGSSL/boringssl/.openssl/include/openssl/ssl.h&amp;#34; \
  &amp;amp;&amp;amp; make -j$(getconf _NPROCESSORS_ONLN) \
  &amp;amp;&amp;amp; make install \
  &amp;amp;&amp;amp; rm -rf /etc/nginx/html/ \
  &amp;amp;&amp;amp; mkdir /etc/nginx/conf.d/ \
  &amp;amp;&amp;amp; mkdir -p /usr/share/nginx/html/ \
  &amp;amp;&amp;amp; install -m644 html/index.html /usr/share/nginx/html/ \
  &amp;amp;&amp;amp; install -m644 html/50x.html /usr/share/nginx/html/ \
  &amp;amp;&amp;amp; ln -s /usr/lib/nginx/modules /etc/nginx/modules \
  &amp;amp;&amp;amp; strip /usr/sbin/nginx* \
  &amp;amp;&amp;amp; strip /usr/lib/nginx/modules/*.so \
  &amp;amp;&amp;amp; rm -rf /etc/nginx/*.default /etc/nginx/*.so \
  &amp;amp;&amp;amp; rm -rf /usr/src \
  &amp;amp;&amp;amp; apk add --no-cache --virtual .gettext gettext \
  &amp;amp;&amp;amp; mv /usr/bin/envsubst /tmp/ \
  \
  # Determine runtime dependencies
  &amp;amp;&amp;amp; runDeps=&amp;#34;$( \
      scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \
      | awk &amp;#39;{ gsub(/,/, &amp;#34;\nso:&amp;#34;, $2); print &amp;#34;so:&amp;#34; $2 }&amp;#39; \
      | sort -u \
      | xargs -r apk info --installed \
      | sort -u \
    )&amp;#34; \
  &amp;amp;&amp;amp; echo &amp;#34;$runDeps&amp;#34; &amp;gt; /nginx-run-deps.txt \
  \
  # Cleanup
  &amp;amp;&amp;amp; apk del .brotli-build-deps .build-deps .gettext \
  &amp;amp;&amp;amp; rm -rf /root/.cargo \
  &amp;amp;&amp;amp; rm -rf /var/cache/apk/* \
  &amp;amp;&amp;amp; mv /tmp/envsubst /usr/local/bin/ \
  &amp;amp;&amp;amp; mkdir -p /etc/ssl/private \
  &amp;amp;&amp;amp; openssl req -x509 -newkey rsa:4096 -nodes -keyout /etc/ssl/private/localhost.key -out /etc/ssl/localhost.pem -days 365 -sha256 -subj &amp;#39;/CN=localhost&amp;#39; \
  &amp;amp;&amp;amp; ln -sf /dev/stdout /var/log/nginx/access.log \
  &amp;amp;&amp;amp; ln -sf /dev/stderr /var/log/nginx/error.log

# Final Stage
FROM alpine:latest

# Copy Nginx and related files from the builder stage
COPY --from=builder /usr/sbin/nginx /usr/sbin/nginx
COPY --from=builder /etc/nginx /etc/nginx
COPY --from=builder /usr/lib/nginx /usr/lib/nginx
COPY --from=builder /usr/share/nginx /usr/share/nginx
COPY --from=builder /usr/local/bin/envsubst /usr/local/bin/envsubst
COPY --from=builder /var/log/nginx /var/log/nginx
COPY --from=builder /var/cache/nginx /var/cache/nginx
COPY --from=builder /var/run /var/run
COPY --from=builder /etc/ssl /etc/ssl
COPY --from=builder /usr/lib/libssl.so* /usr/lib/
COPY --from=builder /usr/lib/libcrypto.so* /usr/lib/
COPY --from=builder /nginx-run-deps.txt /nginx-run-deps.txt

# Install runtime dependencies
RUN apk add --no-cache ca-certificates openssl libstdc&#43;&#43; libgcc \
  &amp;amp;&amp;amp; apk add --no-cache $(cat /nginx-run-deps.txt) \
  &amp;amp;&amp;amp; rm /nginx-run-deps.txt \
  &amp;amp;&amp;amp; update-ca-certificates \
  \
  # Forward logs to Docker
  &amp;amp;&amp;amp; ln -sf /dev/stdout /var/log/nginx/access.log \
  &amp;amp;&amp;amp; ln -sf /dev/stderr /var/log/nginx/error.log

# Create nginx user and group
RUN addgroup -S nginx \
  &amp;amp;&amp;amp; adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx

# Expose ports
EXPOSE 80 443

STOPSIGNAL SIGTERM

CMD [&amp;#34;nginx&amp;#34;, &amp;#34;-g&amp;#34;, &amp;#34;daemon off;&amp;#34;]</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="nginxconf-sample">Nginx.conf sample</h2>
<p>Here you will find a sample nginx.conf which give you an idea of how i run my nginx environment.</p>
<p>The most important difference in this file compared to the normal nginx.conf should be the brotli compression settings.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">user nginx;
worker_processes auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 1024;
}

http {
	##
	# Basic Settings
	##
	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;

	proxy_connect_timeout  300s;
	proxy_send_timeout  300s;
	proxy_read_timeout  300s;
	fastcgi_send_timeout 300s;
	fastcgi_read_timeout 300s;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##
    log_format  main  &amp;#39;$remote_addr - $remote_user [$time_local] &amp;#34;$request&amp;#34; &amp;#39;
                      &amp;#39;$status $body_bytes_sent &amp;#34;$http_referer&amp;#34; &amp;#39;
                      &amp;#39;&amp;#34;$http_user_agent&amp;#34; &amp;#34;$http_x_forwarded_for&amp;#34;&amp;#39;;

    access_log  /var/log/nginx/access.log  main;

	##
	# Gzip Settings
	##
	gzip on;
	gzip_vary on;
	gzip_proxied any;
	gzip_comp_level 6;
	gzip_buffers 16 8k;
	gzip_http_version 1.1;
	gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml&#43;rss text/javascript;

	##
	# Brotli Settings (modify the brotli_comp_level to match your optimal cpu load vs compression)
	##
	brotli on;
	brotli_static on;
	brotli_comp_level 11;
	brotli_types
	text/plain
	text/css
	text/xml
	text/javascript
	text/x-component
	application/xml
	application/xml&#43;rss
	application/javascript
	application/json
	application/atom&#43;xml
	application/vnd.ms-fontobject
	application/x-font-ttf
	application/x-font-opentype
	application/x-font-truetype
	application/x-web-app-manifest&#43;json
	application/xhtml&#43;xml
	application/octet-stream
	font/opentype
	font/truetype
	font/eot
	font/otf
	font/woff
	font/woff2
	image/svg&#43;xml
	image/x-icon
	image/vnd.microsoft.icon
	image/bmp;

	##
	# Virtual Host Configs
	##
	include /etc/nginx/conf.d/*.conf;
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="siteconf-sample">Site.conf sample</h2>
<p>You need to watch out that the higher you set the brotli compression the more cpu intensive it becomes. TTFB (time to first byte) can become an issue when the compression ratio is set to high with to slow kubernetes/ high loads or everything in between. so you need to test on real world scenario&rsquo;s and adjust accordingly. This is not only a Brotli issue but Gzip also has the same disadvantages when setting compression levels to high while the decrease in file size isn&rsquo;t worth it.</p>
<p>You can use this file to take all the HTTP/3 settings defined here below. This will optimize and enable HTTP/3 support for your site.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">server {
       	# HTTP/3 and Quic Listen
      	listen 443 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;

        # Certificate locations for HTTPS.
	    ssl_certificate /etc/ssl/certs/wildcard-cert-23.crt;
        ssl_certificate_key /etc/ssl/private/wildcard-cert-23.key;

        # Enable SSL settings for performance.
        ssl_session_cache shared:SSL:50m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
        # Enable SSL settings for security.
        ssl_prefer_server_ciphers on;
        ssl_protocols TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        # Base settings (root and hostname).
        root /var/www/html;
        index index.php index.html index.htm;
        server_name kubernetes.servername.nl;

        # HTTP/2 settings
        http2_push_preload on;
        # /End of HTTP/2.

        # HTTP/3 settings
        # Enable TLSv1.3&amp;#39;s 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	    }

        location ~ \.php$ {
                try_files $uri =404;
                # Bunch of PHP and FPM settings
                fastcgi_split_path_info ^(.&#43;\.php)(/.&#43;)$;
                fastcgi_pass your-php-fpm-instance:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info
                
                # Add a bunch of other headers based on your desired settings.
                add_header X-protocol $server_protocol always;   
                add_header X-Cache $upstream_cache_status always;
                add_header X-Frame-Options SAMEORIGIN always;
                add_header X-Content-Type-Options nosniff always;
                add_header X-XSS-Protection &amp;#34;1; mode=block&amp;#34;;
                add_header Strict-Transport-Security &amp;#34;max-age=31536000; includeSubdomains; preload&amp;#34; always;
                add_header Referrer-Policy strict-origin-when-cross-origin;
                add_header Pragma public;
                add_header Cache-Control &amp;#34;public&amp;#34;;
        }
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="multiple-h3-connections-from-a-single-nginx">Multiple H3 Connections from a single Nginx</h2>
<p>Here i will explain how you can make multiple server blocks within Nginx work with HTTP/3. This has some caveats since http3 uses UDP with the setting reuseport which makes it unable to assign port 443 multiple times when using HTTP/3</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>This part of the header informs that our HTTP/3 connection accepts port 443. When you have a single nginx container setup and you have more server blocks configured you will get issues regarding the reuse for port 443.</p>
<p>With Http3 you can only define a single port once within a server block. So if you have more server blocks you have the option to let your website communicate over different ports then 443 using this alt-svc header which tells the browser it accepts HTTP/3 and on which port.</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;

add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:444&amp;#34;; ma=86400, h3=&amp;#34;:444&amp;#34;; ma=86400&amp;#39; always;</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span></span></pre><p>Check out the differences in the h3 ports here. This way you can make multiple server blocks use h3 by specifying unique ports.</p>
<p>All you need to do is make your instance listen to the right port specified in your alt-svc header. If there is a successful connection possible the browser will try to switch to h3. Below a sample how you can listen to port 443 and 444 with HTTP3</p>
<pre class="line-numbers language-nginx"><code class="language-nginx">server {

        server_name http3-port-443

       	# HTTP/3 and Quic Listen
      	listen 443 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:443&amp;#34;; ma=86400, h3=&amp;#34;:443&amp;#34;; ma=86400&amp;#39; always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;
        
        # HTTP/3 settings
        # Enable TLSv1.3&amp;#39;s 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	}
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><pre class="line-numbers language-nginx"><code class="language-nginx">server {

        server_name http3-port-444

       	# HTTP/3 and Quic Listen
      	listen 444 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc &amp;#39;h2=&amp;#34;:443&amp;#34;; ma=86400, h3-29=&amp;#34;:444&amp;#34;; ma=86400, h3=&amp;#34;:444&amp;#34;; ma=86400&amp;#39; always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;
        
        # HTTP/3 settings
        # Enable TLSv1.3&amp;#39;s 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	}
}</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><p>It is that easy to make your Reverse proxy or multiple server blocks establish a H3 connection. You do have to make the right port forwards within your firewall/network also.</p>
<p>When using docker you configure this as shown below. When specifying UDP you have to specify every port seperately in docker. Below is my sample which includes UDP ports 443 up to 449.</p>
<pre class="line-numbers language-yaml"><code class="language-yaml">services:
  ############################################################
  # proxy webservice to connect to multiple nginx instances
  ############################################################
  reverse-proxy:
    image: custom-build-nginx:latest
    restart: unless-stopped
    ports:
      - &amp;#39;80:80&amp;#39;
      - &amp;#39;443:443&amp;#39;
      - &amp;#39;443:443/udp&amp;#39;
      - &amp;#39;444:444/udp&amp;#39;
      - &amp;#39;445:445/udp&amp;#39;
      - &amp;#39;446:446/udp&amp;#39;
      - &amp;#39;447:447/udp&amp;#39;
      - &amp;#39;448:448/udp&amp;#39;
      - &amp;#39;449:449/udp&amp;#39;
    volumes:</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="update-03-04-2024">Update (03-04-2024)</h2>
<p>Added some versioning to my dockerfile to better compare versions. I also cleaned up the build from yesterday and finally decided to add a ENV for setting the boringssl version also. Since their build broke my dockerfile we better update it manually. I also updated nginx to 1.25.4 and NJS to 0.8.3 and verified the build</p>
<h2 id="update-02-04-2024">Update (02-04-2024)</h2>
<p>I only recently found out that an update to BoringSSL done around 28 february broke my build of BoringSSL. below the updated dockerfile that compiles again. I had to build libssl as a shared library for it to function again. I will cleanup this image a bit more but it is at least working again.</p>
<h2 id="update-18-03-2024">Update (18-03-2024)</h2>
<p>Added a section to explain how you can use multiple H3 connections when you have a Nginx that has more than 1 server block. This also holds the key when you use Nginx as a Reverse Proxy and want to make use of H3.</p>
<h2 id="update-04-09-2023">Update (04-09-2023)</h2>
<p>I found out that this build didnt compile anymore due to a change in the brotli source code? It might had something to do with updates from github i didnt version in this Dockerfile. I completely reworked this file to work with the updated Nginx 1.25.2 which has &ldquo;built in&rdquo; HTTP/3 support. I also made sure we have native Brotli support again. This should keep functioning from now on.</p>
<h2 id="update-30-08-2023">Update (30-08-2023)</h2>
<p>I removed the modsec support there was before. This was giving random Errors and since i wont use it i removed it. It might return later.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The ultimate ZSH and Windows Terminal look and feel</title>
      <link>https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/</link>
      <guid isPermaLink="false">64180e15dba1d1003d7c5261</guid>
      <pubDate>Wed, 08 Feb 2023 10:57:00 +0100</pubDate>
      <dc:creator>&lt;![CDATA[Remco Loup.]]></dc:creator>
      <category>&lt;![CDATA[Under the hood]]></category>
      <description>&lt;![CDATA[In this post i will explain my particular setup of how i use my windows terminal along with some other tips. So if you want to impress someone while working in your favorite coffee place by making them feel like you work for Nasa.]]></description>
      <media:content url="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/feature.jpg" medium="image"/>
      <content:encoded>&lt;![CDATA[<h2 id="introduction">Introduction</h2>
<p>In this post i will explain my particular setup of how i use my windows terminal along with some other tips. So if you want to impress someone while working in your favorite coffee place by making them feel like you work for Nasa. You are in the right place! All jokes aside, working with something day in day out it at least may look more appealing if possible, right?</p>
<p>Lets start with a list below of everything i have installed after which i will explain what they all do.</p>
<h2 id="zsh-and-the-p10k-theme">ZSH and the p10k theme</h2>
<figure class="bookmark"><a class="bookmark__link" href="https://github.com/ohmyzsh/ohmyzsh" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - ohmyzsh/ohmyzsh: 🙃 A delightful community-driven (with 2,100&#43; contributors) framework for managing your zsh configuration. Includes 300&#43; optional plugins (rails, git, macOS, hub, docker, homebrew, node, php, python, etc), 140&#43; themes to spice up your morning, and an auto-update tool so that makes it easy to keep up with the latest updates from the community.</span><span class="bookmark__description">🙃 A delightful community-driven (with 2,100&#43; contributors) framework for managing your zsh configuration. Includes 300&#43; optional plugins (rails, git, macOS, hub, docker, homebrew, node, php, pyth…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/01-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>ohmyzsh</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/01-thumb_hu_cd46afdcd6f277f0.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/romkatv/powerlevel10k" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - romkatv/powerlevel10k: A Zsh theme</span><span class="bookmark__description">A Zsh theme. Contribute to romkatv/powerlevel10k development by creating an account on GitHub.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/02-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>romkatv</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/02-thumb_hu_e0ce4be87804921b.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://spacevim.org/quick-start-guide/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Quick start guide | SpaceVim</span><span class="bookmark__description">A quick start guide which will tell you how to install and configure SpaceVim, also provides a list of resources for learning SpaceVim.</span><span class="bookmark__meta"><span>SpaceVim</span><span>Shidong Wang</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/03-thumb_hu_fd18ea080be99d12.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<p>I have installed the above items for the look and feel of my Shell. I use Windows Terminal and this makes my terminal look good and distinguishes the right information with colors added to the shell.</p>
<p><strong>ohmyzsh</strong>is the framework for managing my ZSH configuration. The theme i installed is p10k. This is a highly customizable theme.</p>
<p><strong>SpaceVim</strong>is one the of the best visual editors for editing everything you need in Linux. Go check it out by using the link.</p>
<p>Here a screenshot of how my personal shell looks like. I will also share the Json config below of my windows terminal. The font i use is the default nerd font which comes with p10k. This is the Meslo Gorgeous monospace Nerd font.</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-1_hu_98a6094c3d3897a5.webp" alt="" width="560" height="342" data-zoom-src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-1_hu_f6fef50f1a2399a5.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>The Colors i use are called Atom One Dark. And are a subtle pastel colored font</p>
<figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image_hu_6020c119054673fb.webp" alt="" width="400" height="70" data-zoom-src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image_hu_6020c119054673fb.webp" loading="lazy" decoding="async">
  </picture></figure>

<p>In the json file below you can find some nice benefits in making your experience even more customized.</p>
<h2 id="kali-linux">Kali linux</h2>
<p>I have Kali linux installed working in a seamless overlay. This means you can run Kali visually native within your Windows. See the screenshot below how neat this looks! Follow the link below for how to get this working with Kali.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://www.kali.org/docs/wsl/win-kex/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">Win-KeX | Kali Linux Documentation</span><span class="bookmark__description">Windows Subsystem for Linux 2 &amp; Win-KeX</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/04-icon_hu_5cd2a9b225aaa8ff.webp" alt="" class="bookmark__icon" width="32" height="32" loading="lazy" decoding="async">
  </picture><span>Kali Linux</span></span></span><span class="bookmark__thumb"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/04-thumb.svg" sizes="250px" alt="" loading="lazy" decoding="async"></span></a></figure>

<pre class="line-numbers language-json"><code class="language-json">&amp;#34;commandline&amp;#34;: &amp;#34;wsl -d kali-linux kex --sl --wtstart -s&amp;#34;,</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><figure class="figure"><picture><source type="image/avif" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><source type="image/webp" sizes="(max-width: 47.99rem) calc(100vw - 2.5rem), 720px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-2_hu_727fbce17435cc82.webp" alt="" width="560" height="351" data-zoom-src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/images/image-2_hu_b5bbf743fe36003f.webp" loading="lazy" decoding="async">
  </picture><figcaption>Seamless integration with kali and Windows</figcaption></figure>

<h2 id="external-ssh-in-windows-terminal">External SSH in Windows Terminal</h2>
<p>You can also easily login to (external) servers requiring tokens with Windows Terminal.</p>
<pre class="line-numbers language-json"><code class="language-json">&amp;#34;commandline&amp;#34;: &amp;#34;ssh -i \&amp;#34;~/.ssh/id_rsa\&amp;#34; username@ip-address&amp;#34;,</code><span class="line-numbers-rows" aria-hidden="true"><span></span></span></pre><p>Look at how to configure this using the link below.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://blog.anurut.com/ssh-with-private-key-in-windows-terminal/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">How to SSH into a remote server using private key and Windows Terminal</span><span class="bookmark__description">SSH into a remote computer in just one click using a private key and Windows Terminal | Super easy.</span><span class="bookmark__meta"><span>./anurut&#39;s-blog</span><span>Anurut</span></span></span></a></figure>

<h2 id="windows-terminal-config">Windows Terminal config</h2>
<p>Here is the json for the Windows Terminal with the last part the color theme i am using. and the settings for the look and feel i use. Also present are examples for running kali seamless and an SSH session with tokens.</p>
<pre class="line-numbers language-json"><code class="language-json">    &amp;#34;profiles&amp;#34;: 
    {
        &amp;#34;defaults&amp;#34;: 
        {
            &amp;#34;antialiasingMode&amp;#34;: &amp;#34;cleartype&amp;#34;,
            &amp;#34;backgroundImage&amp;#34;: null,
            &amp;#34;backgroundImageAlignment&amp;#34;: &amp;#34;bottom&amp;#34;,
            &amp;#34;backgroundImageOpacity&amp;#34;: 0.8,
            &amp;#34;colorScheme&amp;#34;: &amp;#34;Atom One Dark&amp;#34;,
            &amp;#34;cursorShape&amp;#34;: &amp;#34;vintage&amp;#34;,
            &amp;#34;experimental.retroTerminalEffect&amp;#34;: false,
            &amp;#34;font&amp;#34;: 
            {
                &amp;#34;face&amp;#34;: &amp;#34;MesloLGS NF&amp;#34;
            },
            &amp;#34;opacity&amp;#34;: 75,
            &amp;#34;useAcrylic&amp;#34;: true
        },
        &amp;#34;list&amp;#34;: 
        [
            {
                &amp;#34;commandline&amp;#34;: &amp;#34;wsl -d kali-linux kex --sl --wtstart -s&amp;#34;,
                &amp;#34;guid&amp;#34;: &amp;#34;{55ca431a-3a87-5fb3-83cd-11ececc031d2}&amp;#34;,
                &amp;#34;hidden&amp;#34;: false,
                &amp;#34;icon&amp;#34;: &amp;#34;C:\\Users\\icons\\kali-menu.png&amp;#34;,
                &amp;#34;name&amp;#34;: &amp;#34;Kali Seamless overlay&amp;#34;,
                &amp;#34;startingDirectory&amp;#34;: &amp;#34;%USERPROFILE%&amp;#34;
            },
            {
                &amp;#34;commandline&amp;#34;: &amp;#34;ssh -i \&amp;#34;~/.ssh/id_rsa\&amp;#34; username@ip-address&amp;#34;,
                &amp;#34;guid&amp;#34;: &amp;#34;{0caa0dad-35be-5f56-a8ff-afceaeaa6103}&amp;#34;,
                &amp;#34;hidden&amp;#34;: false,
                &amp;#34;name&amp;#34;: &amp;#34;External test server&amp;#34;
            }
        ]
    },
    &amp;#34;schemes&amp;#34;: 
    [
        {
            &amp;#34;background&amp;#34;: &amp;#34;#282C34&amp;#34;,
            &amp;#34;black&amp;#34;: &amp;#34;#000000&amp;#34;,
            &amp;#34;blue&amp;#34;: &amp;#34;#61AFEF&amp;#34;,
            &amp;#34;brightBlack&amp;#34;: &amp;#34;#5C6370&amp;#34;,
            &amp;#34;brightBlue&amp;#34;: &amp;#34;#61AFEF&amp;#34;,
            &amp;#34;brightCyan&amp;#34;: &amp;#34;#56B6C2&amp;#34;,
            &amp;#34;brightGreen&amp;#34;: &amp;#34;#98C379&amp;#34;,
            &amp;#34;brightPurple&amp;#34;: &amp;#34;#C678DD&amp;#34;,
            &amp;#34;brightRed&amp;#34;: &amp;#34;#E06C75&amp;#34;,
            &amp;#34;brightWhite&amp;#34;: &amp;#34;#FFFFFF&amp;#34;,
            &amp;#34;brightYellow&amp;#34;: &amp;#34;#D19A66&amp;#34;,
            &amp;#34;cursorColor&amp;#34;: &amp;#34;#FFFFFF&amp;#34;,
            &amp;#34;cyan&amp;#34;: &amp;#34;#56B6C2&amp;#34;,
            &amp;#34;foreground&amp;#34;: &amp;#34;#CCCCCC&amp;#34;,
            &amp;#34;green&amp;#34;: &amp;#34;#98C379&amp;#34;,
            &amp;#34;name&amp;#34;: &amp;#34;Atom One Dark&amp;#34;,
            &amp;#34;purple&amp;#34;: &amp;#34;#C678DD&amp;#34;,
            &amp;#34;red&amp;#34;: &amp;#34;#E06C75&amp;#34;,
            &amp;#34;selectionBackground&amp;#34;: &amp;#34;#FFFFFF&amp;#34;,
            &amp;#34;white&amp;#34;: &amp;#34;#ABB2BF&amp;#34;,
            &amp;#34;yellow&amp;#34;: &amp;#34;#D19A66&amp;#34;
        }
    ],
    &amp;#34;themes&amp;#34;: [],
    &amp;#34;useAcrylicInTabRow&amp;#34;: true</code><span class="line-numbers-rows" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></pre><h2 id="addins-which-make-your-shell-look-and-function-even-better">Addins which make your shell look and function even better</h2>
<p>Below you will find some more links which are going to make your shell experience even better. It is a collection of tools i use daily. You can already look at the bookmarks and get an idea of what it does. I would recommend to read through every link and decide if this is also going to make your life easier.</p>
<p>I personally like Exa the most. This is a replacement for the &rsquo;ls&rsquo; command which creates a lot more visibility using colors and icons for folders and such. Also htop is something i already use for years as the goto process manager.</p>
<figure class="bookmark"><a class="bookmark__link" href="https://github.com/BurntSushi/ripgrep" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - BurntSushi/ripgrep: ripgrep recursively searches directories for a regex pattern while respecting your gitignore</span><span class="bookmark__description">ripgrep recursively searches directories for a regex pattern while respecting your gitignore - GitHub - BurntSushi/ripgrep: ripgrep recursively searches directories for a regex pattern while respec…</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/06-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>BurntSushi</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/06-thumb_hu_b211a5b1ef787f02.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/nvbn/thefuck" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - nvbn/thefuck: Magnificent app which corrects your previous console command.</span><span class="bookmark__description">Magnificent app which corrects your previous console command. - GitHub - nvbn/thefuck: Magnificent app which corrects your previous console command.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/07-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>nvbn</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/07-thumb_hu_b2b48ab666833f4.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/rupa/z" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - rupa/z: z - jump around</span><span class="bookmark__description">z - jump around. Contribute to rupa/z development by creating an account on GitHub.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/08-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>rupa</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/08-thumb_hu_4400b46cb8d5382e.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://github.com/ogham/exa" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">GitHub - ogham/exa: A modern replacement for ‘ls’.</span><span class="bookmark__description">A modern replacement for ‘ls’. Contribute to ogham/exa development by creating an account on GitHub.</span><span class="bookmark__meta"><picture><source type="image/avif" sizes="25px"><source type="image/webp" sizes="25px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/09-icon_hu_a8d8b8bb716d7df8.webp" alt="" class="bookmark__icon" width="50" height="50" loading="lazy" decoding="async">
  </picture><span>GitHub</span><span>ogham</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/09-thumb_hu_67f6b8479bab6f37.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

<figure class="bookmark"><a class="bookmark__link" href="https://htop.dev/" rel="noopener"><span class="bookmark__body"><span class="bookmark__title">htop - an interactive process viewer</span><span class="bookmark__description"></span><span class="bookmark__meta"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/10-icon.ico" sizes="25px" alt="" class="bookmark__icon" loading="lazy" decoding="async"><span>an interactive process viewer</span></span></span><span class="bookmark__thumb"><picture><source type="image/avif" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/webp" media="(max-width: 47.99rem)" sizes="calc(100vw - 2.5rem)"><source type="image/avif" sizes="250px"><source type="image/webp" sizes="250px"><img src="https://www.allsubjectsmatter.nl/the-ultimate-zsh-and-windows-terminal-look-and-feel/bookmarks/10-thumb_hu_745f32083f045968.webp" alt="" width="500" height="435" loading="lazy" decoding="async">
  </picture></span></a></figure>

]]></content:encoded>
    </item>
  </channel>
</rss>
