<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alessandro Mainardi</title>
    <description>The latest articles on DEV Community by Alessandro Mainardi (@alexmaina).</description>
    <link>https://dev.to/alexmaina</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3741769%2F83e0f172-e01a-4634-94ae-53b4e192b011.jpeg</url>
      <title>DEV Community: Alessandro Mainardi</title>
      <link>https://dev.to/alexmaina</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexmaina"/>
    <language>en</language>
    <item>
      <title>How to Fix WordPress "Too Many Redirects" in Docker behind Reverse Proxy</title>
      <dc:creator>Alessandro Mainardi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:49:05 +0000</pubDate>
      <link>https://dev.to/alexmaina/how-to-fix-wordpress-too-many-redirects-in-docker-behind-reverse-proxy-27io</link>
      <guid>https://dev.to/alexmaina/how-to-fix-wordpress-too-many-redirects-in-docker-behind-reverse-proxy-27io</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Original Reference:&lt;/strong&gt; Originally published with full architecture diagrams and server configuration templates at &lt;a href="https://alexmaina.dev/wordpress-docker-redirect-loop/" rel="noopener noreferrer"&gt;alexmaina.dev/wordpress-docker-redirect-loop&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Running WordPress inside Docker behind a reverse proxy (like &lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;, &lt;strong&gt;Traefik&lt;/strong&gt;, &lt;strong&gt;Caddy&lt;/strong&gt;, or &lt;strong&gt;Cloudflare&lt;/strong&gt;) often leads to the dreaded browser error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERR_TOO_MANY_REDIRECTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser gets trapped in an infinite HTTP 301 loop. Here is why this happens and how to permanently fix it in under 60 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ The 30-Second Solution
&lt;/h2&gt;

&lt;p&gt;Open your WordPress volume mount and edit &lt;code&gt;wp-config.php&lt;/code&gt;. Paste this snippet &lt;strong&gt;BEFORE&lt;/strong&gt; the line &lt;code&gt;/* That's all, stop editing! Happy publishing. */&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Trust HTTPS termination from your Reverse Proxy&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_X_FORWARDED_PROTO'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_X_FORWARDED_PROTO'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'https'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTPS'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'on'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_HOME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://yourdomain.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_SITEURL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://yourdomain.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FORCE_SSL_ADMIN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. Anatomy of the Loop: Why Does it Happen?
&lt;/h2&gt;

&lt;p&gt;In a modern containerized stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user requests &lt;code&gt;https://yourdomain.com&lt;/code&gt; (encrypted on port &lt;strong&gt;443&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;The Reverse Proxy terminates the SSL session (decrypts the traffic) and forwards the request internally to the WordPress container via HTTP on port &lt;strong&gt;80&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Inside the container, PHP checks &lt;code&gt;$_SERVER['HTTPS']&lt;/code&gt;. Because internal Docker network communication is unencrypted, &lt;code&gt;$_SERVER['HTTPS']&lt;/code&gt; is empty or &lt;code&gt;off&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;WordPress sees that its configured site URL is HTTPS, thinks the visitor accessed via insecure HTTP, and responds with an &lt;code&gt;HTTP 301 Moved Permanently&lt;/code&gt; directing back to &lt;code&gt;https://yourdomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The browser follows the redirect, the proxy intercepts it again on port 443, forwards it via port 80, and the cycle repeats infinitely until the browser throws &lt;code&gt;ERR_TOO_MANY_REDIRECTS&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. Nginx Reverse Proxy Headers
&lt;/h2&gt;

&lt;p&gt;Ensure your upstream Nginx proxy passes the correct forwarded protocol headers:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;/div&gt;



&lt;p&gt;If using &lt;strong&gt;Nginx Proxy Manager (NPM)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under &lt;strong&gt;Details&lt;/strong&gt;: Scheme is &lt;code&gt;http&lt;/code&gt;, Forward Host is the container name, Port is &lt;code&gt;80&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;SSL&lt;/strong&gt;: Enable &lt;em&gt;Force SSL&lt;/em&gt; and &lt;em&gt;HTTP/2 Support&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The Cloudflare Trap: Flexible vs Full (Strict)
&lt;/h2&gt;

&lt;p&gt;If your DNS is proxied through &lt;strong&gt;Cloudflare&lt;/strong&gt; (orange cloud active):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never use Flexible SSL:&lt;/strong&gt; Flexible instructs Cloudflare to contact your origin server strictly on port 80 (HTTP). If WordPress enforces HTTPS, Cloudflare and your origin will bounce redirects back and forth indefinitely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Change your Cloudflare SSL/TLS encryption mode to &lt;strong&gt;Full&lt;/strong&gt; or &lt;strong&gt;Full (Strict)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Verification with cURL
&lt;/h2&gt;

&lt;p&gt;Run this terminal command to test the raw HTTP status without browser cache pollution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-ILs&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} -&amp;gt; %{url_effective}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200 -&amp;gt; https://yourdomain.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see 20 consecutive &lt;code&gt;301 -&amp;gt; ...&lt;/code&gt; lines, the PHP block was either placed too late in &lt;code&gt;wp-config.php&lt;/code&gt; or the reverse proxy is stripping the &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; header.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Author: &lt;a href="https://alexmaina.dev/about/" rel="noopener noreferrer"&gt;Alessandro Mainardi&lt;/a&gt; — Software Developer &amp;amp; Computer Engineering Student @ UniBo. Explore more self-hosting &amp;amp; Docker guides at &lt;a href="https://alexmaina.dev/" rel="noopener noreferrer"&gt;alexmaina.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>wordpress</category>
      <category>nginx</category>
      <category>devops</category>
    </item>
    <item>
      <title>Access Your Homelab Remotely without Port Forwarding (Bypass CGNAT with Tailscale)</title>
      <dc:creator>Alessandro Mainardi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:48:18 +0000</pubDate>
      <link>https://dev.to/alexmaina/access-your-homelab-remotely-without-port-forwarding-bypass-cgnat-with-tailscale-43e8</link>
      <guid>https://dev.to/alexmaina/access-your-homelab-remotely-without-port-forwarding-bypass-cgnat-with-tailscale-43e8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Original Reference:&lt;/strong&gt; Originally published with zero-trust network topologies at &lt;a href="https://alexmaina.dev/tailscale-pihole-homelab-cgnat/" rel="noopener noreferrer"&gt;alexmaina.dev/tailscale-pihole-homelab-cgnat&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With modern fiber (FTTH), cellular 5G, or Starlink connections, most ISPs place customer routers behind &lt;strong&gt;CGNAT (Carrier-Grade NAT)&lt;/strong&gt;. Under CGNAT, you do not have a public IPv4 address, rendering classic port forwarding completely useless.&lt;/p&gt;

&lt;p&gt;Even if you have a public IP, opening ports (like port 22 or WireGuard port 51820) exposes your home router to 24/7 automated port scanners and brute-force attacks.&lt;/p&gt;

&lt;p&gt;Here is how to set up a &lt;strong&gt;Tailscale Subnet Router&lt;/strong&gt; with &lt;strong&gt;Pi-hole MagicDNS&lt;/strong&gt; to gain zero-trust, encrypted remote access to every device on your home network without opening a single port.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why a Subnet Router?
&lt;/h2&gt;

&lt;p&gt;Instead of installing the Tailscale client on every single device (which is impossible for IoT gear, network printers, IP cameras, or legacy hardware), a &lt;strong&gt;Subnet Router&lt;/strong&gt; acts as a secure gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One always-on Linux host (Raspberry Pi, mini PC, or VM) acts as the bridge.&lt;/li&gt;
&lt;li&gt;When you enable Tailscale on your laptop or phone while travelling, you can access &lt;code&gt;192.168.1.x&lt;/code&gt; IPs exactly as if you were on your home Wi-Fi.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Configure the Linux Gateway
&lt;/h2&gt;

&lt;p&gt;SSH into your Linux machine and execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install Tailscale&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tailscale.com/install.sh | sh

&lt;span class="c"&gt;# 2. Enable IP packet forwarding in Linux kernel&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'net.ipv4.ip_forward = 1'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/sysctl.d/99-tailscale.conf
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'net.ipv6.conf.all.forwarding = 1'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/sysctl.d/99-tailscale.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/sysctl.d/99-tailscale.conf

&lt;span class="c"&gt;# 3. Advertise your local subnet (adjust 192.168.1.0/24 to your LAN subnet)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;tailscale up &lt;span class="nt"&gt;--advertise-routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.1.0/24 &lt;span class="nt"&gt;--accept-dns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Approve Subnet Routes in Tailscale Console
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://login.tailscale.com/admin/machines" rel="noopener noreferrer"&gt;Tailscale Admin Machines Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Locate your gateway device and look for the &lt;strong&gt;Subnets&lt;/strong&gt; badge.&lt;/li&gt;
&lt;li&gt;Click the three dots &lt;strong&gt;(...)&lt;/strong&gt; → &lt;strong&gt;Edit route settings...&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Check the box for your subnet (e.g. &lt;code&gt;192.168.1.0/24&lt;/code&gt;) and click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Integrate Pi-hole for Global Ad-Blocking &amp;amp; Local DNS
&lt;/h2&gt;

&lt;p&gt;If you have &lt;strong&gt;Pi-hole&lt;/strong&gt; or &lt;strong&gt;AdGuard Home&lt;/strong&gt; running in your homelab:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Tailscale Admin Console, go to &lt;strong&gt;DNS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Nameservers&lt;/strong&gt;, click &lt;strong&gt;Add nameserver&lt;/strong&gt; → &lt;strong&gt;Custom...&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your Pi-hole's local IP (e.g. &lt;code&gt;192.168.1.50&lt;/code&gt;) or Tailscale IP (&lt;code&gt;100.x.y.z&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Toggle &lt;strong&gt;Override local DNS&lt;/strong&gt; ON.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Result:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Seamless encrypted access to Proxmox, NAS storage, and local servers from anywhere.&lt;/li&gt;
&lt;li&gt;Network-wide ad-blocking and tracker filtering active on your phone and laptop wherever you travel.&lt;/li&gt;
&lt;li&gt;Zero ports forwarded on your home router.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Author: &lt;a href="https://alexmaina.dev/about/" rel="noopener noreferrer"&gt;Alessandro Mainardi&lt;/a&gt; — Software Developer &amp;amp; Computer Engineering Student @ UniBo. Explore homelab setups at &lt;a href="https://alexmaina.dev/" rel="noopener noreferrer"&gt;alexmaina.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailscale</category>
      <category>homelab</category>
      <category>networking</category>
      <category>security</category>
    </item>
    <item>
      <title>QNAP Container Station: Free Up Ports 80 &amp; 443 for Docker Reverse Proxy</title>
      <dc:creator>Alessandro Mainardi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:47:26 +0000</pubDate>
      <link>https://dev.to/alexmaina/qnap-container-station-free-up-ports-80-443-for-docker-reverse-proxy-53b7</link>
      <guid>https://dev.to/alexmaina/qnap-container-station-free-up-ports-80-443-for-docker-reverse-proxy-53b7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Original Reference:&lt;/strong&gt; Originally published with complete step-by-step storage configurations at &lt;a href="https://alexmaina.dev/qnap-docker-reverse-proxy/" rel="noopener noreferrer"&gt;alexmaina.dev/qnap-docker-reverse-proxy&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When trying to deploy a Reverse Proxy (like &lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;, &lt;strong&gt;Traefik&lt;/strong&gt;, or &lt;strong&gt;Caddy&lt;/strong&gt;) inside QNAP's &lt;strong&gt;Container Station&lt;/strong&gt;, Docker will fail with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error response from daemon: driver failed programming external connectivity on endpoint: bind: address already in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens because QNAP's operating system (&lt;strong&gt;QTS&lt;/strong&gt; or &lt;strong&gt;QuTS hero&lt;/strong&gt;) occupies port 80 and port 443 by default for its administrative interface and built-in web server.&lt;/p&gt;

&lt;p&gt;Here is how to liberate both ports cleanly and set up a centralized reverse proxy without breaking access to your NAS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Move QTS Admin System Ports
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Log into your QTS web dashboard.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Control Panel&lt;/strong&gt; → &lt;strong&gt;System&lt;/strong&gt; → &lt;strong&gt;General Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under the &lt;strong&gt;System Administration&lt;/strong&gt; section:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System port (HTTP):&lt;/strong&gt; Change from &lt;code&gt;8080&lt;/code&gt; (or &lt;code&gt;80&lt;/code&gt;) to &lt;code&gt;8088&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable secure connection (HTTPS):&lt;/strong&gt; Change from &lt;code&gt;443&lt;/code&gt; (or &lt;code&gt;4433&lt;/code&gt;) to &lt;strong&gt;&lt;code&gt;8443&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncheck:&lt;/strong&gt; &lt;em&gt;"Redirect HTTP connection to HTTPS"&lt;/em&gt; if bound to port 80.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Apply&lt;/strong&gt;. Your browser will reload at &lt;code&gt;https://NAS-IP:8443&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 2: Disable the Built-in Apache Web Server
&lt;/h2&gt;

&lt;p&gt;Even with admin ports moved, QTS runs a background Apache server that listens on port 80.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Control Panel, navigate to &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;Web Server&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Uncheck &lt;strong&gt;Enable Web Server&lt;/strong&gt; (or change its listening ports to &lt;code&gt;8085&lt;/code&gt; and &lt;code&gt;8445&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Apply&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ports 80 and 443 are now completely free on the physical network interface!&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Deploy Nginx Proxy Manager with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Container Station&lt;/strong&gt; → &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;, and paste this clean &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;npm-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jc21/nginx-proxy-manager:latest'&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-proxy-manager&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;80:80'&lt;/span&gt;    &lt;span class="c1"&gt;# Public HTTP port now available&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;443:443'&lt;/span&gt;  &lt;span class="c1"&gt;# Public HTTPS port now available&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;81:81'&lt;/span&gt;    &lt;span class="c1"&gt;# NPM Web Admin Dashboard&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_MYSQL_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm-db"&lt;/span&gt;
      &lt;span class="na"&gt;DB_MYSQL_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3306&lt;/span&gt;
      &lt;span class="na"&gt;DB_MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
      &lt;span class="na"&gt;DB_MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SuperSecureDBPassword2026!"&lt;/span&gt;
      &lt;span class="na"&gt;DB_MYSQL_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/share/Container/npm/data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/share/Container/npm/letsencrypt:/etc/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm-db&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;homelab-net&lt;/span&gt;

  &lt;span class="na"&gt;npm-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mariadb:10.11'&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm-database&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RootSecretPassword2026!"&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SuperSecureDBPassword2026!"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/share/Container/npm/mysql:/var/lib/mysql&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;homelab-net&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Route Container Traffic Internally
&lt;/h2&gt;

&lt;p&gt;With all your containers attached to &lt;code&gt;homelab-net&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You &lt;strong&gt;no longer need to expose ports&lt;/strong&gt; to the host for individual services (WordPress, Nextcloud, etc.).&lt;/li&gt;
&lt;li&gt;Access NPM dashboard at &lt;code&gt;http://NAS-IP:81&lt;/code&gt; (Default credentials: &lt;code&gt;admin@example.com&lt;/code&gt; / &lt;code&gt;changeme&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Proxy Host&lt;/strong&gt; with your domain name and set &lt;em&gt;Forward Hostname&lt;/em&gt; to the internal container name (e.g. &lt;code&gt;wordpress-app&lt;/code&gt;) on port &lt;code&gt;80&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Request a free &lt;strong&gt;Let's Encrypt SSL certificate&lt;/strong&gt; directly through NPM.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Author: &lt;a href="https://alexmaina.dev/about/" rel="noopener noreferrer"&gt;Alessandro Mainardi&lt;/a&gt; — Software Developer &amp;amp; Computer Engineering Student @ UniBo. Explore homelab architectures at &lt;a href="https://alexmaina.dev/" rel="noopener noreferrer"&gt;alexmaina.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>qnap</category>
      <category>docker</category>
      <category>homelab</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Speed Up Python CI/CD 10x with uv: Migration from Pip &amp; Poetry</title>
      <dc:creator>Alessandro Mainardi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:46:44 +0000</pubDate>
      <link>https://dev.to/alexmaina/speed-up-python-cicd-10x-with-uv-migration-from-pip-poetry-18n0</link>
      <guid>https://dev.to/alexmaina/speed-up-python-cicd-10x-with-uv-migration-from-pip-poetry-18n0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Original Reference:&lt;/strong&gt; Originally published with performance benchmarks and deployment pipelines at &lt;a href="https://alexmaina.dev/python-uv-github-actions/" rel="noopener noreferrer"&gt;alexmaina.dev/python-uv-github-actions&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In standard Python CI/CD pipelines, &lt;code&gt;pip install&lt;/code&gt; or &lt;code&gt;poetry install&lt;/code&gt; is notoriously slow, burning 2 to 5 billable minutes on every commit just resolving wheel dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;uv&lt;/strong&gt;, created in Rust by Astral (the authors of Ruff), is an extremely fast Python package and project manager designed as a drop-in replacement for Pip, Pip-tools, Virtualenv, and Poetry.&lt;/p&gt;

&lt;p&gt;Here is how to migrate an existing project, lock dependencies with &lt;code&gt;uv.lock&lt;/code&gt;, and reduce your GitHub Actions workflow times from minutes to seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Quick Migration Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install uv (Linux/macOS)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh

&lt;span class="c"&gt;# Install uv (Windows PowerShell)&lt;/span&gt;
powershell &lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt; ByPass &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"irm https://astral.sh/uv/install.ps1 | iex"&lt;/span&gt;

&lt;span class="c"&gt;# Initialize standard PEP 621 project&lt;/span&gt;
uv init my-project
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project

&lt;span class="c"&gt;# Add dependencies with automatic lockfile update&lt;/span&gt;
uv add fastapi uvicorn &lt;span class="s2"&gt;"pydantic&amp;gt;=2.0"&lt;/span&gt;
uv add &lt;span class="nt"&gt;--dev&lt;/span&gt; pytest ruff

&lt;span class="c"&gt;# Run tests instantly in isolated environment&lt;/span&gt;
uv run pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Optimized GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;, leverage the official &lt;code&gt;astral-sh/setup-uv&lt;/code&gt; action with built-in cache keying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test &amp;amp; Lint Python&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup uv with automatic dependency caching&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest"&lt;/span&gt;
          &lt;span class="na"&gt;enable-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;cache-dependency-glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uv.lock"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install required Python version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv python install &lt;/span&gt;&lt;span class="m"&gt;3.12&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sync dependencies from lockfile&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv sync --frozen --all-extras --dev&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint with Ruff&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run ruff check .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Pytest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv run pytest --maxfail=1 -q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Production Multi-Stage Dockerfile
&lt;/h2&gt;

&lt;p&gt;Use uv during the build stage to compile wheels and copy strictly the finalized &lt;code&gt;.venv&lt;/code&gt; into the production runtime container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# STAGE 1: Dependency resolution &amp;amp; build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ghcr.io/astral-sh/uv:python3.12-bookworm-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; UV_COMPILE_BYTECODE=1 \&lt;/span&gt;
    UV_LINK_MODE=copy

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--no-install-project&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt;

&lt;span class="c"&gt;# STAGE 2: Minimal runtime image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim-bookworm&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.venv /app/.venv&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/app/.venv/bin:$PATH"&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Author: &lt;a href="https://alexmaina.dev/about/" rel="noopener noreferrer"&gt;Alessandro Mainardi&lt;/a&gt; — Software Developer &amp;amp; Computer Engineering Student @ UniBo. Explore modern tooling and software engineering guides at &lt;a href="https://alexmaina.dev/" rel="noopener noreferrer"&gt;alexmaina.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>githubactions</category>
      <category>devops</category>
      <category>rust</category>
    </item>
    <item>
      <title>Keep Your Microsoft 365 E5 Developer Tenant Active Automatically</title>
      <dc:creator>Alessandro Mainardi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:45:43 +0000</pubDate>
      <link>https://dev.to/alexmaina/keep-your-microsoft-365-e5-developer-tenant-active-automatically-1n9i</link>
      <guid>https://dev.to/alexmaina/keep-your-microsoft-365-e5-developer-tenant-active-automatically-1n9i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Original Reference:&lt;/strong&gt; Originally published with security architecture baselines at &lt;a href="https://alexmaina.dev/m365-developer-rinnovo-automatico/" rel="noopener noreferrer"&gt;alexmaina.dev/m365-developer-rinnovo-automatico&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;strong&gt;Microsoft 365 Developer Program&lt;/strong&gt; provides a free 25-user E5 enterprise sandbox subscription (worth over $1,300/year). However, it comes with a strict condition: &lt;strong&gt;it expires every 90 days unless Microsoft detects continuous developer activity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your sandbox goes idle, Microsoft terminates the tenant, permanently erasing your users, mailboxes, and configurations.&lt;/p&gt;

&lt;p&gt;Here is an automated, serverless solution using &lt;strong&gt;Microsoft Graph API&lt;/strong&gt; and &lt;strong&gt;GitHub Actions&lt;/strong&gt; to generate valid developer telemetry and maintain your tenant indefinitely.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Register a Daemon App in Microsoft Entra ID
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign into the &lt;a href="https://entra.microsoft.com" rel="noopener noreferrer"&gt;Microsoft Entra Admin Center&lt;/a&gt; as tenant admin.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Identity&lt;/strong&gt; → &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;App registrations&lt;/strong&gt; → &lt;strong&gt;New registration&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;M365-KeepAlive-Automation&lt;/code&gt; (Accounts in this organizational directory only).&lt;/li&gt;
&lt;li&gt;Save the &lt;strong&gt;Application (client) ID&lt;/strong&gt; and &lt;strong&gt;Directory (tenant) ID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Certificates &amp;amp; secrets&lt;/strong&gt;, create a new client secret (24-month validity) and copy its value.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;API permissions&lt;/strong&gt; → &lt;strong&gt;Add permission&lt;/strong&gt; → &lt;strong&gt;Microsoft Graph&lt;/strong&gt; → &lt;strong&gt;Application permissions&lt;/strong&gt;, select:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;User.Read.All&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Calendars.ReadWrite&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Directory.Read.All&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Grant admin consent for [Your Tenant]&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. Python Telemetry Script (keepalive.py)
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;keepalive.py&lt;/code&gt; in a private GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M365_TENANT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M365_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M365_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Acquire OAuth2 Token via Client Credentials Flow
&lt;/span&gt;&lt;span class="n"&gt;token_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://login.microsoftonline.com/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TENANT_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/oauth2/v2.0/token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;token_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grant_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://graph.microsoft.com/.default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ Microsoft Graph token successfully acquired.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Query tenant user directory
&lt;/span&gt;&lt;span class="n"&gt;users_res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://graph.microsoft.com/v1.0/users?$top=5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users_res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📡 Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; users in tenant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Create simulated developer calendar event
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;target_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;event_payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dev Sprint Sync - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTML&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Automated Graph API Developer Telemetry.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dateTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeZone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dateTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeZone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;event_res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://graph.microsoft.com/v1.0/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target_user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event_payload&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📅 Developer telemetry event created in calendar.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. GitHub Actions Cron Workflow
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;.github/workflows/keepalive.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;M365 E5 KeepAlive Automation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Runs Monday, Wednesday, Friday at 08:30 UTC&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1,3,5'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;telemetry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install requests&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Graph Telemetry&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;M365_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.M365_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;M365_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.M365_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;M365_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.M365_CLIENT_SECRET }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python keepalive.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store &lt;code&gt;M365_TENANT_ID&lt;/code&gt;, &lt;code&gt;M365_CLIENT_ID&lt;/code&gt;, and &lt;code&gt;M365_CLIENT_SECRET&lt;/code&gt; under &lt;strong&gt;Repository Settings&lt;/strong&gt; → &lt;strong&gt;Secrets and variables&lt;/strong&gt; → &lt;strong&gt;Actions&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Author: &lt;a href="https://alexmaina.dev/about/" rel="noopener noreferrer"&gt;Alessandro Mainardi&lt;/a&gt; — Software Developer &amp;amp; Computer Engineering Student @ UniBo. Explore cloud identity guides at &lt;a href="https://alexmaina.dev/" rel="noopener noreferrer"&gt;alexmaina.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>githubactions</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Fix Error AADSTS50011 in Microsoft Entra ID (Redirect URI Mismatch)</title>
      <dc:creator>Alessandro Mainardi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:43:35 +0000</pubDate>
      <link>https://dev.to/alexmaina/how-to-fix-error-aadsts50011-in-microsoft-entra-id-redirect-uri-mismatch-3c2h</link>
      <guid>https://dev.to/alexmaina/how-to-fix-error-aadsts50011-in-microsoft-entra-id-redirect-uri-mismatch-3c2h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Original Reference:&lt;/strong&gt; Originally published with interactive diagrams and enterprise identity baselines at &lt;a href="https://alexmaina.dev/entra-id-redirect-uri-aadsts50011/" rel="noopener noreferrer"&gt;alexmaina.dev/entra-id-redirect-uri-aadsts50011&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you are developing applications using &lt;strong&gt;MSAL (Microsoft Authentication Library)&lt;/strong&gt; and Microsoft Entra ID (formerly Azure AD), you have likely run into this frustrating blocker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application '{client-id}'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This error appears during the OAuth 2.0 / OpenID Connect authorization handshake. Microsoft's token endpoint performs a &lt;strong&gt;strict character-by-character validation&lt;/strong&gt;, meaning even a single trailing slash or port mismatch will immediately halt authentication.&lt;/p&gt;

&lt;p&gt;Here is how to diagnose the root cause and fix it in under 3 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The 4 Most Common Causes of Mismatch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Root Cause&lt;/th&gt;
&lt;th&gt;What MSAL Request Sends&lt;/th&gt;
&lt;th&gt;What Entra ID App Has Configured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trailing Slash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:3000/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol Scheme&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://app.mydomain.com/callback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://app.mydomain.com/callback&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamic Dev Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http://localhost:5173&lt;/code&gt; (Vite)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;http://localhost:3000&lt;/code&gt; (Create-React-App)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wrong Platform Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SPA with PKCE (CORS)&lt;/td&gt;
&lt;td&gt;Configured under "Web" section&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Step-by-Step Fix via Microsoft Entra Admin Center
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;a href="https://entra.microsoft.com" rel="noopener noreferrer"&gt;Microsoft Entra Admin Center&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Identity&lt;/strong&gt; → &lt;strong&gt;Applications&lt;/strong&gt; → &lt;strong&gt;App registrations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;All applications&lt;/strong&gt; tab and open your registered application (match the &lt;code&gt;client-id&lt;/code&gt; from your error log).&lt;/li&gt;
&lt;li&gt;In the left navigation menu, click &lt;strong&gt;Authentication&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Inspect your platform configuration:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-Page Application (SPA):&lt;/strong&gt; If you are using React, Vue, Angular, or vanilla JS with MSAL Browser (Auth Code flow with PKCE), your URI &lt;strong&gt;MUST&lt;/strong&gt; be under the &lt;em&gt;Single-page application&lt;/em&gt; section. If registered under "Web", token requests will fail due to missing CORS headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web:&lt;/strong&gt; Use this only for backend-rendered apps with client secrets (Next.js server-side, Node/Express, Django, Flask, ASP.NET Core).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile and desktop applications:&lt;/strong&gt; Required for CLI tools, PowerShell, Python scripts, or desktop apps using loopback listeners (&lt;code&gt;http://localhost&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Paste the exact Redirect URI from the error message.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; at the top.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Fast Fix with Microsoft Graph PowerShell
&lt;/h2&gt;

&lt;p&gt;If you frequently rotate ports during local testing, you can update redirect URIs programmatically using the official Microsoft Graph PowerShell SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Authenticate to Microsoft Graph&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Connect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application.ReadWrite.All"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$AppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR-APPLICATION-CLIENT-ID"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-MgApplication&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appId eq '&lt;/span&gt;&lt;span class="nv"&gt;$AppId&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Configure allowed redirect URIs for local dev + production&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SpaRedirects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5173"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://alexmaina.dev/auth/callback"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Update SPA Platform configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Update-MgApplication&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ApplicationId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Spa&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RedirectUris&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SpaRedirects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✅ Redirect URIs successfully updated for: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Crucial Rules for Localhost in Entra ID
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP is strictly reserved for Localhost:&lt;/strong&gt; Microsoft Entra ID enforces HTTPS for all production domain redirect URIs. The only exception is loopback addresses (&lt;code&gt;http://localhost&lt;/code&gt; or &lt;code&gt;http://127.0.0.1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wildcards are deprecated:&lt;/strong&gt; Microsoft no longer allows wildcard characters (&lt;code&gt;*&lt;/code&gt;) in redirect URIs for security reasons (preventing token interception and open-redirect attacks). Each callback endpoint must be explicitly declared.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Need Deep Dive Architecture?
&lt;/h3&gt;

&lt;p&gt;For full zero-trust setups, enterprise RBAC baselines, and tenant isolation, check out my technical guide hub at &lt;a href="https://alexmaina.dev/" rel="noopener noreferrer"&gt;alexmaina.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Author: &lt;a href="https://alexmaina.dev/about/" rel="noopener noreferrer"&gt;Alessandro Mainardi&lt;/a&gt; — Software Developer &amp;amp; Computer Engineering Student @ Alma Mater Studiorum - University of Bologna.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>entra</category>
      <category>oauth</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
