<?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: brent alexis</title>
    <description>The latest articles on DEV Community by brent alexis (@ba796).</description>
    <link>https://dev.to/ba796</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%2F4166918%2Ff179ab39-fb12-48f3-bad2-90db5716ecfd.png</url>
      <title>DEV Community: brent alexis</title>
      <link>https://dev.to/ba796</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ba796"/>
    <language>en</language>
    <item>
      <title>4 times the "success" message lied to me</title>
      <dc:creator>brent alexis</dc:creator>
      <pubDate>Thu, 29 Oct 2026 15:21:34 +0000</pubDate>
      <link>https://dev.to/ba796/4-times-the-success-message-lied-to-me-2knn</link>
      <guid>https://dev.to/ba796/4-times-the-success-message-lied-to-me-2knn</guid>
      <description>&lt;p&gt;I built and tested Nginx and Apache configs on a small side project, and one lesson kept coming back. A green tick only tells you the part that ran is happy. It doesn't tell you the thing you wanted actually happened.&lt;/p&gt;

&lt;p&gt;Here are four times I got caught. All real, all in the last few weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Nginx reload exited 0 and kept the old config.
&lt;/h2&gt;

&lt;p&gt;I changed the key on a rate limit zone from &lt;code&gt;$binary_remote_addr&lt;/code&gt; to &lt;code&gt;$server_name&lt;/code&gt;. &lt;code&gt;nginx -t&lt;/code&gt; passed. &lt;code&gt;nginx -s reload&lt;/code&gt; exited 0. Nothing looked wrong.&lt;/p&gt;

&lt;p&gt;But Nginx was still running the old config. The only sign of trouble was one line in the error log:&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;[emerg]&lt;/span&gt; &lt;span class="s"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;"one"&lt;/span&gt; &lt;span class="s"&gt;uses&lt;/span&gt; &lt;span class="s"&gt;the&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$server_name&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="s"&gt;key&lt;/span&gt; &lt;span class="s"&gt;while&lt;/span&gt; &lt;span class="s"&gt;previously&lt;/span&gt; &lt;span class="s"&gt;it&lt;/span&gt; &lt;span class="s"&gt;used&lt;/span&gt; &lt;span class="s"&gt;the&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="s"&gt;key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reload command just sent a signal. Exit 0 meant the signal got there, not that the new config was accepted. So nauseating. The fix was a full restart, or giving the zone a new name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; look at the error log after every reload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5 /var/log/nginx/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Git push worked and the site didn't change.
&lt;/h2&gt;

&lt;p&gt;I pushed a change to my page titles. No errors. Opened the site... old titles.&lt;/p&gt;

&lt;p&gt;The host's build had failed at the "Cloning" step with "error occurred while fetching repository". A temporary hiccup on their side - "Retry build" fixed it.&lt;/p&gt;

&lt;p&gt;A successful push only proves GitHub has your code. It says nothing about the deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; open the live page (or view source) after every deploy, not just from the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;nginx -t&lt;/code&gt; said ok, but my block rule never ran.
&lt;/h2&gt;

&lt;p&gt;Nginx checks regex locations in the order they appear and uses the first one that matches. My hidden-file block sat below &lt;code&gt;location ~ \.php$&lt;/code&gt;, so a request like &lt;code&gt;/.old/backup.php&lt;/code&gt; matched the PHP block first and never reached my rule. The syntax was perfect. The rule just never got a turn.&lt;/p&gt;

&lt;p&gt;The fix was moving the protection blocks above the PHP block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; curl the exact paths you meant to block and look at the status code.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The request was blocked, just not by me.
&lt;/h2&gt;

&lt;p&gt;Testing on Apache, &lt;code&gt;/.htaccess&lt;/code&gt; came back 403. Blocked, great. Except my rule returned 404.&lt;/p&gt;

&lt;p&gt;The 403 came from Apache's own built-in rule for &lt;code&gt;.ht*&lt;/code&gt; files, which kicked in before any .htaccess rewrites ran. If I'd only checked "blocked or not", I'd have counted it as a pass for my rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt; have your rules return a status you can tell apart (I use 404) so you know which rule actually answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same lesson, four times
&lt;/h2&gt;

&lt;p&gt;Anyway, the lesson learnt - check the result from outside the thing you changed. The command that reports success is usually the part that didn't fail.&lt;/p&gt;

&lt;p&gt;After that, I put all of this into small Docker lab scripts that spun up real nginx:1.27 and httpd:2.4 containers and curled every path. The last run was 42/42, after fixing the one expectation from #4. They tested the output of the free config generators I built at &lt;a href="https://proxyforge.tech" rel="noopener noreferrer"&gt;ProxyForge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which "success" message got you?&lt;/p&gt;

&lt;p&gt;This post was drafted with AI help, edited by me.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>apache</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Nginx reverse proxy + HTTPS in 5 minutes (video + every command)</title>
      <dc:creator>brent alexis</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:53:00 +0000</pubDate>
      <link>https://dev.to/ba796/nginx-reverse-proxy-https-in-5-minutes-video-every-command-46on</link>
      <guid>https://dev.to/ba796/nginx-reverse-proxy-https-in-5-minutes-video-every-command-46on</guid>
      <description>&lt;p&gt;I recorded a short walkthrough of putting Nginx in front of an app and getting a free Let's Encrypt certificate onto it. Everything in the video runs on a real Ubuntu server, nothing is staged.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/lO_qUszpfEA" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Below are the exact commands used, so you can copy them instead of pausing the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Linux server. I used &lt;strong&gt;Ubuntu 24.04&lt;/strong&gt;, which ships &lt;strong&gt;Nginx 1.24&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A domain or subdomain whose &lt;strong&gt;A record points at the server&lt;/strong&gt;. If it's on Cloudflare, keep the record &lt;strong&gt;DNS only (grey cloud)&lt;/strong&gt; while you get the certificate.&lt;/li&gt;
&lt;li&gt;An app listening locally, for example on &lt;code&gt;127.0.0.1:3000&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Install Nginx and Certbot. Open the firewall to allow Nginx
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx certbot
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No app installed? Use this to start a test one on port 3000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h1&amp;gt;Hello from port 3000&amp;lt;/h1&amp;gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/app/index.html
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave it running. Open a second SSH window for the rest of the tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check your Nginx version
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;This matters for HTTPS. Nginx &lt;strong&gt;1.25.1 and newer&lt;/strong&gt; use a separate &lt;code&gt;http2 on;&lt;/code&gt; line. &lt;strong&gt;Older versions&lt;/strong&gt; (like 1.24 on Ubuntu 24.04, or 22.04 and Debian 11 packages) need HTTP/2 on the listen line instead:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get this wrong and &lt;code&gt;nginx -t&lt;/code&gt; fails with &lt;code&gt;unknown directive "http2"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generate the config
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://proxyforge.tech" rel="noopener noreferrer"&gt;ProxyForge&lt;/a&gt;, the free generator I built. Pick &lt;strong&gt;Reverse proxy&lt;/strong&gt;, enter your domain and &lt;code&gt;127.0.0.1:3000&lt;/code&gt;, and tick &lt;strong&gt;Redirect HTTP to HTTPS&lt;/strong&gt;, &lt;strong&gt;WebSocket&lt;/strong&gt;, and &lt;strong&gt;Older Nginx&lt;/strong&gt; if step 2 showed a version before 1.25.1. Any config that does the same job works too. The parts that matter are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a port 80 block that answers Let's Encrypt's check (&lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt;) and redirects everything else to HTTPS (more on this in step 7)&lt;/li&gt;
&lt;li&gt;a port 443 block with &lt;code&gt;ssl_certificate&lt;/code&gt; / &lt;code&gt;ssl_certificate_key&lt;/code&gt; pointing at &lt;code&gt;/etc/letsencrypt/live/&amp;lt;your-domain&amp;gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proxy_pass http://127.0.0.1:3000;&lt;/code&gt; plus the usual &lt;code&gt;proxy_set_header&lt;/code&gt; lines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy the output and keep it handy (I paste it into Notepad++), but I don't install it yet. The certificate files it points to don't exist, so &lt;code&gt;nginx -t&lt;/code&gt; would fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Get the certificate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="nt"&gt;--webroot&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; /var/www/html &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; app.example.com &lt;span class="nt"&gt;-m&lt;/span&gt; you@example.com &lt;span class="nt"&gt;--agree-tos&lt;/span&gt; &lt;span class="nt"&gt;--no-eff-email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webroot method only drops a file in &lt;code&gt;/var/www/html&lt;/code&gt;, which Ubuntu's default site already serves on port 80. Certbot never edits your Nginx config. You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/app.example.com/fullchain.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Install the config, test, then reload
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/app.example.com     &lt;span class="c"&gt;# paste, Ctrl+O, Enter, Ctrl+X&lt;/span&gt;
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; means Nginx only reloads if the test passes, so a typo can't take a running site down.&lt;/p&gt;

&lt;p&gt;One gotcha that cost me a few minutes. If the paste into nano silently fails, you end up saving an &lt;strong&gt;empty file&lt;/strong&gt;. &lt;code&gt;nginx -t&lt;/code&gt; still passes, but nothing listens on 443. Check with &lt;code&gt;grep -c . /etc/nginx/sites-available/app.example.com&lt;/code&gt;. If it prints 0, paste again.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Check it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://app.example.com     &lt;span class="c"&gt;# expect: 301 Moved Permanently -&amp;gt; https://&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://app.example.com    &lt;span class="c"&gt;# expect: HTTP/2 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open it in a browser and click the padlock to see the Let's Encrypt certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make sure renewal works
&lt;/h2&gt;

&lt;p&gt;Certbot renews automatically, but a common trap is the HTTPS redirect itself. If port 80 redirects &lt;strong&gt;everything&lt;/strong&gt; to HTTPS, Let's Encrypt's renewal check gets passed through to your app instead of &lt;code&gt;/var/www/html&lt;/code&gt;, and renewal quietly fails about 60 days later.&lt;/p&gt;

&lt;p&gt;I recorded the video &lt;strong&gt;before&lt;/strong&gt; I caught this, so the config on screen redirects everything. ProxyForge's output now includes the exception (tested against a real &lt;code&gt;nginx -t&lt;/code&gt; before it went live), so regenerate the config if you copied it from the video. If you're using another config, the port 80 block should look like this:&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;location&lt;/span&gt; &lt;span class="n"&gt;/.well-known/acme-challenge/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&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;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;return&lt;/code&gt; has to sit inside &lt;code&gt;location /&lt;/code&gt;. A &lt;code&gt;return&lt;/code&gt; placed directly in the server block runs before Nginx looks at any location, so the check would still be redirected.&lt;/p&gt;

&lt;p&gt;Not sure which folder you gave Certbot? This version serves the file if it's there and otherwise falls back to the redirect, so it can't make things worse. ProxyForge's Redirect Generator uses this form:&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;location&lt;/span&gt; &lt;span class="n"&gt;/.well-known/acme-challenge/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="s"&gt;@redirect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;@redirect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;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;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, test renewal now instead of finding out in two months:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Congratulations, all simulated renewals succeeded&lt;/code&gt; means you're done.&lt;/p&gt;




&lt;p&gt;If you hit an error I didn't cover, paste it in the comments and I'll take a look. The generator is free with no sign-up, and the &lt;a href="https://proxyforge.tech/docs" rel="noopener noreferrer"&gt;how-to guide&lt;/a&gt; explains every option.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>devops</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Apache SSL line that looks right and still won't start</title>
      <dc:creator>brent alexis</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:45:00 +0000</pubDate>
      <link>https://dev.to/ba796/the-apache-ssl-line-that-looks-right-and-still-wont-start-58dc</link>
      <guid>https://dev.to/ba796/the-apache-ssl-line-that-looks-right-and-still-wont-start-58dc</guid>
      <description>&lt;p&gt;I built a free config generator for Nginx and Apache. Before shipping the SSL part, I ran every output against a real Nginx 1.27 and Apache 2.4 server. Using AI, I built a test sandbox instead of just reading the configs over.&lt;/p&gt;

&lt;p&gt;That saved me more than once. The Apache version had two errors, both on the same line, and both looked completely fine on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The goal was simple enough. Take a Let's Encrypt certificate and produce the Apache config for HTTPS with OCSP stapling turned on. Stapling lets your server send the certificate's "still valid" proof along with the handshake, so the visitor's browser doesn't have to go and ask the certificate authority itself.&lt;/p&gt;

&lt;p&gt;On Apache, stapling needs two directives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SSLUseStapling on&lt;/code&gt; turns stapling on.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SSLStaplingCache&lt;/code&gt; tells Apache where to keep the stapled responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Version 1 put everything inside the site's &lt;code&gt;&amp;lt;VirtualHost *:443&amp;gt;&lt;/code&gt; block, the way a lot of snippets online do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;VirtualHost&lt;/span&gt;&lt;span class="sr"&gt; *:443&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;SSLEngine&lt;/span&gt; &lt;span class="ss"&gt;on&lt;/span&gt;
    &lt;span class="nc"&gt;SSLCertificateFile&lt;/span&gt;    /etc/letsencrypt/live/example.com/fullchain.pem
    &lt;span class="nc"&gt;SSLCertificateKeyFile&lt;/span&gt; /etc/letsencrypt/live/example.com/privkey.pem
    &lt;span class="nc"&gt;SSLUseStapling&lt;/span&gt; &lt;span class="ss"&gt;on&lt;/span&gt;
    &lt;span class="nc"&gt;SSLStaplingCache&lt;/span&gt; "shared:SSL:10m"   &lt;span class="c"&gt;# two errors on this one line&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;VirtualHost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads fine. But it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 1: Wrong place
&lt;/h2&gt;

&lt;p&gt;Apache didn't start. &lt;code&gt;apachectl configtest&lt;/code&gt; stopped with a syntax error on the &lt;code&gt;SSLStaplingCache&lt;/code&gt; line, saying the directive is "not allowed here".&lt;/p&gt;

&lt;p&gt;The reason is scope. In Apache's mod_ssl, &lt;code&gt;SSLUseStapling&lt;/code&gt; is allowed inside a &lt;code&gt;&amp;lt;VirtualHost&amp;gt;&lt;/code&gt;, but &lt;code&gt;SSLStaplingCache&lt;/code&gt; is server config only. It sets up one cache shared by the whole server, so it must sit in the main config: &lt;code&gt;httpd.conf&lt;/code&gt;, or &lt;code&gt;ssl.conf&lt;/code&gt; / &lt;code&gt;mods-available/ssl.conf&lt;/code&gt; on Debian and Ubuntu. Put it inside any &lt;code&gt;&amp;lt;VirtualHost&amp;gt;&lt;/code&gt; and Apache refuses the file.&lt;/p&gt;

&lt;p&gt;It's an easy mistake to make, because the two directives belong together, so they naturally end up next to each other in the same block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 2: Wrong syntax
&lt;/h2&gt;

&lt;p&gt;After moving the line out of the &lt;code&gt;&amp;lt;VirtualHost&amp;gt;&lt;/code&gt;, Apache still rejected it. This time it was the value.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shared:SSL:10m&lt;/code&gt; is Nginx syntax, borrowed from Nginx's &lt;code&gt;ssl_session_cache&lt;/code&gt; directive. Apache uses its own cache providers, and the common one is &lt;code&gt;shmcb&lt;/code&gt; (shared memory, cyclic buffer), written as a path plus a size in bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;SSLStaplingCache&lt;/span&gt; "shmcb:/tmp/ssl_stapling_cache(32768)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're building something like this, you often write the Nginx and Apache outputs side by side. I was using AI to speed things up, got a bit lazy, and the Nginx habit slipped into the Apache side. A developer who knows Nginx might read &lt;code&gt;shared:SSL:10m&lt;/code&gt; and move on. Apache doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug in my own test setup
&lt;/h2&gt;

&lt;p&gt;One more lesson came from the sandbox itself. For a while, Apache seemed to just exit with no error at all. My test container had &lt;code&gt;ErrorLog&lt;/code&gt; pointing at a file, so when Apache died during startup, the error was written to a log file inside a container that had already stopped.&lt;/p&gt;

&lt;p&gt;Pointing &lt;code&gt;ErrorLog&lt;/code&gt; at &lt;code&gt;/dev/stderr&lt;/code&gt; for testing fixed it immediately, and the real error messages showed up. If you test Apache in Docker, do that first. Save yourself the headache!&lt;/p&gt;

&lt;p&gt;While I was in there, the testing also showed that the Apache output was missing &lt;code&gt;SSLSessionCache&lt;/code&gt;, which the Nginx side already had. It's the same kind of directive (server config only), and it uses &lt;code&gt;shmcb&lt;/code&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config that works
&lt;/h2&gt;

&lt;p&gt;The fix was to split the output into two clearly labelled parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once, in the main server config (outside any &lt;code&gt;&amp;lt;VirtualHost&amp;gt;&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;SSLSessionCache&lt;/span&gt;  "shmcb:/tmp/ssl_scache(512000)"
&lt;span class="nc"&gt;SSLStaplingCache&lt;/span&gt; "shmcb:/tmp/ssl_stapling_cache(32768)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inside your &lt;code&gt;&amp;lt;VirtualHost *:443&amp;gt;&lt;/code&gt; block:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;SSLEngine&lt;/span&gt; &lt;span class="ss"&gt;on&lt;/span&gt;
&lt;span class="nc"&gt;SSLCertificateFile&lt;/span&gt;    /etc/letsencrypt/live/example.com/fullchain.pem
&lt;span class="nc"&gt;SSLCertificateKeyFile&lt;/span&gt; /etc/letsencrypt/live/example.com/privkey.pem
&lt;span class="nc"&gt;SSLProtocol&lt;/span&gt; -all +TLSv1.2 +TLSv1.3
&lt;span class="nc"&gt;SSLUseStapling&lt;/span&gt; &lt;span class="ss"&gt;on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test before you reload, so a mistake never takes the site down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apachectl configtest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload apache2   &lt;span class="c"&gt;# httpd on RHEL/Alma/Rocky/Fedora&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; means the reload only happens if the test passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope rules are invisible when you read config.&lt;/strong&gt; Nothing about &lt;code&gt;SSLStaplingCache&lt;/code&gt; looks different from &lt;code&gt;SSLUseStapling&lt;/code&gt;. Only the server knows which context each one is allowed in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing two servers side by side invites cross-contamination.&lt;/strong&gt; Nginx and Apache solve the same problems with different syntax, and it's easy to carry one over to the other. If you do it, test both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Looks right" isn't a test.&lt;/strong&gt; Running the actual binary caught two real errors in one line that several careful read-throughs missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I wrote this post with help from an AI assistant. The testing and results are my own.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you'd rather skip the trial and error, my &lt;a href="https://proxyforge.tech/ssl-generator" rel="noopener noreferrer"&gt;ProxyForge SSL generator&lt;/a&gt; gives you the Certbot command plus the matching Nginx or Apache config, with the Apache output already split into the two parts above. It's free, needs no signup and runs in your browser. The &lt;a href="https://proxyforge.tech/docs#ssl-generator" rel="noopener noreferrer"&gt;how-to guide&lt;/a&gt; explains where each piece goes.&lt;/p&gt;

&lt;p&gt;Have you hit other Apache directives that are only allowed in one context? I'd like to hear about them, since they're exactly the kind of thing worth checking, and maybe adding to my tool.&lt;/p&gt;

</description>
      <category>apache</category>
      <category>devops</category>
      <category>ssl</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
