<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nginx-Proxy-Manager on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/nginx-proxy-manager/</link><description>Recent content in Nginx-Proxy-Manager on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Mon, 31 Mar 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/nginx-proxy-manager/index.xml" rel="self" type="application/rss+xml"/><item><title>Manually adding SSL certs in Nginx Proxy Manager</title><link>https://blog.iankulin.com/manually-adding-ssl-certs-in-nginx-proxy-manager/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/manually-adding-ssl-certs-in-nginx-proxy-manager/</guid><description>&lt;p&gt;A large part of the reason for my use of Nginx Proxy manager over vanilla NGINX, is that it has built-in Let&amp;rsquo;s Encrypt certificate requesting and renewing. This works perfectly for all my public facing services, and until recently, my homelab services. Before I dive into how I&amp;rsquo;ve fixed the problem I ran into, I better explain how my homelab domain is set up, and before I do that, an over-simplified description of how the SSL system works is required&lt;/p&gt;
&lt;h3 id="ssl"&gt;SSL&lt;/h3&gt;
&lt;p&gt;SSL (Secure Socket Layer) on a web site (the little padlock you see in the browser when you visit an https:// site) does three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It tells you that this web site you&amp;rsquo;ve visited is controlled by the same people who own the domain name. This is important to prevent someone hijacking your request to &amp;ldquo;mysecurebank.com&amp;rdquo; and sending it to their password stealing website.&lt;/li&gt;
&lt;li&gt;It encrypts the traffic between the web-browser and the website so it can&amp;rsquo;t be spied on.&lt;/li&gt;
&lt;li&gt;It detects is someone has tried to tamper with the traffic between the web-browser and the website.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For all this to work, there needs to be some way of assuring the certificate issuer that the entity claiming a certificate for the domain, has control of the website that the domain is pointing to. The simplest way to do this is for the certificate issuer to give you a token, you install that in a secret directory on the web server, then the certificate issuer can check it exists. This proves to them that you own the website, and they can issue you the certificate.&lt;/p&gt;
&lt;p&gt;This process is essentially what happens when you use &lt;a href="https://certbot.eff.org/"&gt;Certbot&lt;/a&gt; to obtain a &lt;a href="https://letsencrypt.org/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; SSL certificate. It works great as long as your website is public to the internet so it can be contacted by the certificate issuer. But, that&amp;rsquo;s not the case for my homelab services. They are on an internal network, deliberately not contactable from the wild internet.&lt;/p&gt;
&lt;h3 id="ssl-on-an-internal-network"&gt;SSL on an internal network&lt;/h3&gt;
&lt;p&gt;There is less need for SSL on an internal network. You probably assume there&amp;rsquo;s no one to spy on your traffic, or to fiddle with it, and you know you have control of your apps. Apart from not trusting that, there&amp;rsquo;s a couple of other benefits - you often can&amp;rsquo;t save your passwords for unsecured sites, you can get annoying warning messages, and some apps just straight up won&amp;rsquo;t let you use some functionality without it. This is the case for my Forgejo instance that wants working SSL to allow me to git push to it.&lt;/p&gt;
&lt;h3 id="homelab-ssl"&gt;Homelab SSL&lt;/h3&gt;
&lt;p&gt;There is a way around this - basically we need to assure the certificate issuer that we&amp;rsquo;re in control, but instead of exposing a token on the (unreachable) web server, we add it as a record in the DNS of the domain (called DNS Challenge). The logic of this is that if you control the DNS you control where it points and therefore the web server. This is the first part of the Homelab SSL setup - obtaining the certificate with DNS challenge. The second part is using the DNS to point the domain at an internal website address. My domain and DNS are public - you could enter the domain name in a browser, but it resolves to an address inside my network. This is all much better explained by Wolfgang than me.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/qlcVx-k-02E?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;h3 id="the-problem-ive-run-into"&gt;The problem I&amp;rsquo;ve run into&lt;/h3&gt;
&lt;p&gt;This all worked perfectly when I first set it up. Nginx Proxy Manager (NPM) has a plugin for my domain provider (&lt;a href="http://porkbun.com"&gt;porkbun&lt;/a&gt;) which uses their API to save the token into my DNS settings. The UX for this is that I tell Nginx Proxy Manager that I want to use &amp;ldquo;DNS Challenge&amp;rdquo; for a certificate request, and (by using an API key I&amp;rsquo;ve setup on Porkbun and given to NPM) it does all the fiddling around to obtain the certificate then installs them.&lt;/p&gt;
&lt;p&gt;I must of had that running for a year or so, with the certificates magically being renewed every couple of months with no input from my until just recently. I&amp;rsquo;m not exactly sure what&amp;rsquo;s happened - the error messages that I&amp;rsquo;m not smart enough to sort out suggest that the plugin&amp;rsquo;s operations to install the token at the domain provider is not working. I don&amp;rsquo;t know if it&amp;rsquo;s an API problem, or there&amp;rsquo;s been an NPM update that&amp;rsquo;s broken the plugin, or just something else has changed in my setup. What ever it is, turning everything off and on again, updating everything, and trying manually have not worked. So time for plan B.&lt;/p&gt;
&lt;h3 id="manual-certificates"&gt;Manual Certificates&lt;/h3&gt;
&lt;p&gt;Porkbun (and for all I know other domain sellers) provide a facility to download a &amp;lsquo;certificate bundle&amp;rsquo; directly from them - they are just doing that Let&amp;rsquo;s Encrypt dance directly - missing the NPM and Porkbun API step from the above.&lt;/p&gt;
&lt;p&gt;The certificate bundle contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public.key.pem&lt;/li&gt;
&lt;li&gt;private.key.pem&lt;/li&gt;
&lt;li&gt;domain.cert.pem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And when you go to add a custom certificate in NPM you have these options:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-03-08-at-15.47.04.png" width="981" alt=""&gt;
&lt;p&gt;As you can see your certificate (domain.cert.pem) goes in the certificate slot, and the Certificate Key it&amp;rsquo;s asking for is your private key (private.key.pem). You don&amp;rsquo;t need the intermediate key - this is the same for all Let&amp;rsquo;s Encrypt certificates.&lt;/p&gt;
&lt;p&gt;Doing the certificates this way is less good than having them automatically renewed. Currently Let&amp;rsquo;s Encrypt certificates are good for 90 days, so every three months my monitoring system will let me know they only have a couple of weeks left and I&amp;rsquo;ll have to repeat this manual process. There has been talk of shortening this time which would make that process even more annoying, so hopefully I can sort out the issue in NPM, or find out if Traefik or Caddy have the necessary plugins to do DNS challenge certificates.&lt;/p&gt;
&lt;p&gt;But in the meantime, my internal web apps are all up and secure.&lt;/p&gt;
&lt;h3 id="this-is-not-free"&gt;This is not free&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s Encrypt, and to a lesser extent Certbot have changed the web substantially. Obtaining and installing certificates used to be a difficult and costly process, but these two &amp;lsquo;free&amp;rsquo; services have turned that around. If you run a website and use these services I highly recommend you support the non-profits that keep them in existence as I do.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt - &lt;a href="https://letsencrypt.org/donate/"&gt;have a donation page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Certbot - is provided by the EFF who do other work can be &lt;a href="https://supporters.eff.org/donate/support-work-on-certbot"&gt;donated to here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>NGINX proxy manager - setting headers to use basic auth in your apps</title><link>https://blog.iankulin.com/nginx-proxy-manager-setting-headers-to-use-basic-auth-in-your-apps/</link><pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-proxy-manager-setting-headers-to-use-basic-auth-in-your-apps/</guid><description>&lt;p&gt;When I&amp;rsquo;m spinning up side projects, I frequently ignore auth, and just rely on NGINX basic auth - one of the side benefits of reverse-proxying everything.&lt;/p&gt;
&lt;h3 id="regular-nginx"&gt;Regular NGINX&lt;/h3&gt;
&lt;p&gt;This &lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;article in the docs&lt;/a&gt; explains how to set up basic auth to protect different paths. To make it work in my node apps, I need the successful user name passed in so I check it against the user table to work out access rights etc.&lt;/p&gt;
&lt;p&gt;To get it passed in with every request, we need to stick it in the headers. We do this in the NGINX conf for the site:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	root &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ct&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;example&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	index index&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;html&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	server_name ct&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;example&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	location &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		auth_basic &lt;span style="color:#a3be8c"&gt;&amp;#34;Secure app&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	 auth_basic_user_file &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;nginx&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt;htpasswd&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_pass http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;localhost&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;	
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_http_version &lt;span style="color:#b48ead"&gt;1.1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_set_header Upgrade &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;http_upgrade&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_set_header Connection &lt;span style="color:#a3be8c"&gt;&amp;#39;upgrade&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_set_header Host &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;host&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Username &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;remote_user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		proxy_cache_bypass &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;http_upgrade&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then in my app, I just check the header like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; USERNAME_HEADER &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;x-username&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; basic auth middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; next&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; username &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;headers&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;USERNAME_HEADER&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;400&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Missing header&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;USERNAME_HEADER&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; usersArray&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Save user details to req object
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;role &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;role&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; next&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;401&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User unauthorised&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s a bit of a fudge, but for personal use apps it&amp;rsquo;s quick to set up, and pretty robust from a security point of view.&lt;/p&gt;
&lt;h3 id="nginx-proxy-manager"&gt;NGINX Proxy Manager&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve moved to using NGINX Proxy Manager (NPM) rather than raw NGINX since it makes getting SSL certificates super simple. NPM basically wraps all the reverse proxy functionality of NGINX into a nice click ops web GUI.&lt;/p&gt;
&lt;p&gt;We now specify the user names and passwords in the gui (instead of faffing around installing apache2-utils and running it to build the &lt;code&gt;.htpasswd&lt;/code&gt; file. This is done by creating an access list:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.43.16-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.43.16-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then in the Authorization tab, adding your users:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.42.44-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.42.44-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s saved, we apply it to the proxy record:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.47.38-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.47.38-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But then how do we set the x-username header? Since there&amp;rsquo;s an advanced tab, you may think it&amp;rsquo;s in there, and that looks promising till you read the warning below the text box.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.52.24-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.52.24-am.png" width="839" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Setting a header is exactly what we want to do, so let&amp;rsquo;s head to the &amp;ldquo;locations&amp;rdquo; tab.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.55.04-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-27-at-11.55.04-am.png" width="674" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We just use the root path &amp;ldquo;/&amp;rdquo; for our path so it applies to every request, proxy those requests to our app, and set the header in the text box (which appears when you click the gear button).&lt;/p&gt;</description></item><item><title>Website in a Docker Container</title><link>https://blog.iankulin.com/website-in-a-docker-container/</link><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/website-in-a-docker-container/</guid><description>&lt;p&gt;Having figured out how to use the GitHub package registry, I was a bit inspired by &lt;a href="https://lipanski.com/posts/smallest-docker-image-static-website"&gt;this blog post&lt;/a&gt; from Florin Lipan to deliver all my little static websites as Docker containers. I&amp;rsquo;m not as focused as he is about making them tiny, but I did steal the idea of using &lt;a href="https://busybox.net/about.html"&gt;BusyBox&lt;/a&gt; httpd for serving them, resulting in about 4MB containers. That&amp;rsquo;s small enough for me, and since they are all very similar, there&amp;rsquo;s a fair bit of layer reuse going on.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the setup. I dump the static (html, css, js etc) files for the website into a &amp;lsquo;www&amp;rsquo; sub-directory.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-6.55.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-6.55.02-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The dockerfile pulls in BusyBox then copies those files into the container. Note these are in the container, it&amp;rsquo;s not going to be bound to an external directory (where we could change them), the container carries its website files with it. Any change to the website content will require a container rebuild.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 80&lt;/code&gt; doesn&amp;rsquo;t really do anything, it&amp;rsquo;s pretty much just documentation. Then the &lt;code&gt;CMD&lt;/code&gt; directive starts the server on port 80 and points to the static files that we copied in earlier.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM busybox&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Create the directory for the web content, and copy files in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mkdir &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;p &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY www&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Expose port 80 for the web server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Start the httpd server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sh&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;busybox httpd -f -p 80 -h /var/www/html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To use this dockerfile to build our container, just docker build it and give it a tag:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build -t ghcr.io/iankulin/example.com:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then if we run it, and go to http://localhost, there&amp;rsquo;s our website.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name httpd-example.com -p 80:80 ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="with-nginx-proxy-manager"&gt;With NGINX Proxy Manager&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; file I use on the VPS host is slightly more complicated. We want each of the website containers to run in the same docker network as Nginx Proxy Manager - since docker networks have their own little dns server based on the container names, that&amp;rsquo;s going to make hooking it up trivial.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; example.com:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: httpd-example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - nginx-proxy-manager_default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager_default:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; external: true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="architecture"&gt;Architecture&lt;/h3&gt;
&lt;p&gt;Since I develop on an M1 MacBook, but host all my workloads on regular AMD64 Linux LXC containers or VMs, I need to build for that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build --platform linux/amd64 -t ghcr.io/iankulin/example.com:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In actual fact, I could have built that way for the Mac as well - Docker Desktop would have just run it in a Linux VM with a small performance penalty which wouldn&amp;rsquo;t be noticeable for my purposes. Once it&amp;rsquo;s built, we push it up to the GitHub Container Registry.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Working with the registry is well covered in my previous post, so I won&amp;rsquo;t go into those details here.&lt;/p&gt;
&lt;h3 id="on-the-host"&gt;On the host&lt;/h3&gt;
&lt;p&gt;On the host where the website is to run, I just make a directory for it and drop the &lt;code&gt;docker-compose.yml&lt;/code&gt; in. Then &lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running in the Nginx Proxy Manager docker network, when we specify the host name for the new web site for NPM to proxy to, it&amp;rsquo;s just the container name we gave it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then the DNS settings for your domain need to be pointed to this host. Once that&amp;rsquo;s propagated, you&amp;rsquo;ll be able to request the SSL certificate in NPM and your website is live.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png" width="886" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Containerised NGINX Proxy Manager &amp; the 502 error</title><link>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</link><pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png" width="695" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re used to running NGINX Proxy Manager in front of your web apps, and switch to running it in a container, you&amp;rsquo;re going to need to learn a little about Docker networks to get everything connected. If you just do your regular setup, and direct the proxy for an address to &lt;code&gt;127.0.0.1:&amp;lt;some port&amp;gt;&lt;/code&gt;, it won&amp;rsquo;t exist, and you&amp;rsquo;ll visit your page to find the &amp;ldquo;502 Bad Gateway openresty&amp;rdquo; message.&lt;/p&gt;
&lt;p&gt;If you pause to think for a second it will be obvious why - with NGINX Proxy Manager (I&amp;rsquo;m going to start calling it NPM to save myself some typing) &lt;em&gt;inside&lt;/em&gt; a container, any addresses you&amp;rsquo;re entering into the web interface when setting up proxys are &lt;em&gt;inside&lt;/em&gt; the NPM container. &lt;code&gt;127.0.0.1&lt;/code&gt; from that point of view refers to the inside of the NPM container, and not the host, so you exposing a port from your container to the host is not going to work.&lt;/p&gt;
&lt;p&gt;The fix for this is pretty simple, but first let&amp;rsquo;s look at the exception.&lt;/p&gt;
&lt;h3 id="the-exception"&gt;The Exception&lt;/h3&gt;
&lt;p&gt;Usually the very first proxy I add in NPM is to it&amp;rsquo;s own admin interface on port 81. Since this &lt;em&gt;is&lt;/em&gt; inside the NPM container, the setup looks exactly the same as if you were running on the host, and may well be why you were lulled into a false sense of familiarity.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a little bit confusing, since we can also manually access NPM from &lt;code&gt;http://127.0.0.1:81&lt;/code&gt; on the host if we&amp;rsquo;ve exposed port 81 in our compose file, but it&amp;rsquo;s actually a different route. In fact, we could hide port 81 from the host, and the setting above will still work.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my compose file, notice I haven&amp;rsquo;t exposed port 81&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: &amp;#39;jc21/nginx-proxy-manager:latest&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: nginx-proxy-manager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#39;80:80&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#39;443:443&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./letsencrypt:/etc/letsencrypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So if we try to access port 81 from the host, it won&amp;rsquo;t be answering.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But inside the container, 127.0.0.1 refers to itself, so if we open a shell into the container, the URL http://127.0.0.1:81 refers to something that exists, from there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="getting-out"&gt;Getting Out&lt;/h3&gt;
&lt;p&gt;So how can our NPM point to a service that&amp;rsquo;s running in another container? You are probably used to specifying ports: in your compose file to expose internal container ports to a host, but that doesn&amp;rsquo;t really help us since NPM in a container can not easily access the host&amp;rsquo;s ports. What we&amp;rsquo;d really like is for NPM to be able to access the second container&amp;rsquo;s ports. And in an ideal world, we&amp;rsquo;d like that to be the only way to access them - that way all the access to our second container service is forced through our proxy. Sounds like security no?&lt;/p&gt;
&lt;p&gt;Turns out this is simple thanks to the magic of Docker networks.&lt;/p&gt;
&lt;p&gt;You can get a fair way with Docker without really thinking or knowing about &lt;a href="https://docs.docker.com/engine/network/"&gt;Docker networks&lt;/a&gt;, and I&amp;rsquo;m really only covering the very basics here - you should probably invest some time in learning about this sometime. Meanwhile, lets run the &lt;code&gt;docker network ls&lt;/code&gt; command and see what networks we&amp;rsquo;ve got.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That network &lt;code&gt;nginx-proxy-manager_default&lt;/code&gt; is the one we&amp;rsquo;re interested in. Its name is just the container name with &amp;ldquo;_default&amp;rdquo; added on the end. What we need to do is just make sure the second container is included in that same network. That&amp;rsquo;s a matter of declaring the external network with that name, and including it in our service definition. I&amp;rsquo;m going to use an NGINX server in my example, but it could be anything. Here&amp;rsquo;s the compose for the second container.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-example.com:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: nginx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: nginx-example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./www:/usr/share/nginx/html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./conf/:/etc/nginx/conf.d/:ro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - nginx-proxy-manager_default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager_default:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; external: true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that I haven&amp;rsquo;t exposed any ports to the host here; We&amp;rsquo;re not going to need them since we&amp;rsquo;ll access this container direct from the NPM container via the internal Docker network docker created for us. That little network even contains a DNS server, so we don&amp;rsquo;t even need to worry about the container&amp;rsquo;s IP addresses, we can just use their names.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.52.54-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So any web requests to &amp;ldquo;example.com&amp;rdquo; arriving at our host go to NPM (since I&amp;rsquo;ve exposed ports 80 &amp;amp; 443 - see the top compose file). Then using the proxy I&amp;rsquo;ve added above, they are forwarded to the container named &amp;ldquo;nginx-example.com&amp;rdquo; which is a DNS record inside the Docker network that Docker created for us, and which both the NPM, and my service, containers are members of.&lt;/p&gt;</description></item><item><title>NGINX Proxy Manager</title><link>https://blog.iankulin.com/nginx-proxy-manager/</link><pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-proxy-manager/</guid><description>&lt;p&gt;I&amp;rsquo;ve mentioned using NGINX as an &lt;a href="https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/"&gt;interface between the internet and a service&lt;/a&gt; a while ago. This works by all incoming traffic coming to NGINX, and NGINX determining which service that traffic should go (from the NGINX config files) then acting as a middleman. This functionality is generally referred to as a &amp;lsquo;reverse proxy&amp;rsquo;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/nginx.png" width="959" alt="Terrible drawing of NGINX proxying requests off to different services."&gt;
&lt;p&gt;This is nice for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can have a single point of entry to the services, easier to lock down and secure, with access centrally logged&lt;/li&gt;
&lt;li&gt;The services can be running on all sorts of odd addresses and ports (for example 192.168.101.23:4002) but they can be addressed with sensible names by the user (such as todo.example.com)&lt;/li&gt;
&lt;li&gt;We can add &lt;a href="https://blog.iankulin.com/quick-dirty-auth-with-nginx-node/"&gt;basic auth&lt;/a&gt; to any services that need it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this stuff is managed through the &lt;a href="https://blog.iankulin.com/nginx-config-on-debian-ubuntu/"&gt;NGINX config&lt;/a&gt; files. Perhaps one might look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; listen &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; server_name example&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; location &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; root &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index index&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;html&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; location &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;app2&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_pass http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;&lt;span style="color:#b48ead"&gt;192.168&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;101.65&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;8096&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header Host &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;host&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Real&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#bf616a"&gt;IP&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;remote_addr&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Forwarded&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;For &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;proxy_add_x_forwarded_for&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Forwarded&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Proto &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;scheme&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; location &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;secure_area&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; auth_basic &lt;span style="color:#a3be8c"&gt;&amp;#34;Restricted&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; auth_basic_user_file &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;nginx&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt;htpasswd&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; listen &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; server_name app1&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;example&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; location &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_pass http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;&lt;span style="color:#b48ead"&gt;192.168&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;101.23&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;4000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header Host &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;host&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Real&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#bf616a"&gt;IP&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;remote_addr&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Forwarded&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;For &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;proxy_add_x_forwarded_for&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header X&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Forwarded&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;Proto &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;scheme&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These config files are powerful, and in the usual trade-off somewhat complicated and I&amp;rsquo;ve certainly made problems for myself in the past by making errors in them.&lt;/p&gt;
&lt;p&gt;There is a great project, &lt;a href="https://nginxproxymanager.com/"&gt;NGINX Proxy Manager&lt;/a&gt; that throws a nice web GUI on this process. On top of that, it makes the process of obtaining &lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; SSL certificates even easier than CertBot.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/npm.png" width="946" alt="Terrible drawing of NGINX Proxy Manager proxying requests off to different service, and obtaining SSL certificates for them."&gt;
&lt;p&gt;NGINX Proxy Manager is available as a docker image, and is trivial to set up if you&amp;rsquo;re used to docker. Once that&amp;rsquo;s done, the process of adding the proxies is simple enough that you probably don&amp;rsquo;t need any instructions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-31-at-8.59.56-am.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="alternatives"&gt;Alternatives&lt;/h3&gt;
&lt;p&gt;Rolling your own, or using NGINX Proxy Manager are not your only options. There&amp;rsquo;s also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.haproxy.org/#desc"&gt;HAProxy&lt;/a&gt; - an industrial strength proxy/load balancer&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/quick-starts/reverse-proxy"&gt;Caddy&lt;/a&gt; - same as NGINX but different. Has a great plugin architecture. A particular plugin &lt;a href="https://github.com/lucaslorentz/caddy-docker-proxy"&gt;Caddy-docker-proxy&lt;/a&gt; enables configuration of each service with tables inside the service&amp;rsquo;s docker-compose file which is a particularly neat trick.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://traefik.io/traefik/"&gt;Traefik&lt;/a&gt; - does a similar trick to Caddy-docker-proxy of figuring out it&amp;rsquo;s config from the services it&amp;rsquo;s proxying. It&amp;rsquo;s a serious bit of kit valuable for putting in front of huge Kubernetes swams, and is therefore probably a bit more complex to manage than Caddy-docker-proxy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I haven&amp;rsquo;t used any of these (except NGINX Proxy Manager) so take these descriptions as a starting point only.&lt;/p&gt;
&lt;p&gt;For any self-hosted (at home or on a VPS) services, you are going to need some of this functionality, and NGINX Proxy Manager is a simple, robust approach that should definitely be considered.&lt;/p&gt;</description></item></channel></rss>