<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Chapman's Chatter]]></title><description><![CDATA["✰✰ at best" - New York Times]]></description><link>https://blog.lchapman.dev/</link><image><url>https://blog.lchapman.dev/favicon.png</url><title>Chapman&apos;s Chatter</title><link>https://blog.lchapman.dev/</link></image><generator>Ghost 5.75</generator><lastBuildDate>Thu, 28 May 2026 16:11:52 GMT</lastBuildDate><atom:link href="https://blog.lchapman.dev/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Making a Gate Smart]]></title><description><![CDATA[<p>For this project I will (hopefully) use information from this post: <a href="https://www.aaron-powell.com/posts/2023-01-16-building-a-smart-home---part-7-motorised-gate/?ref=blog.lchapman.dev" rel="noreferrer">Building a Smart Home - Part 7 Motorised Gate by Aaron Powell</a></p><p>Aaron has a <a href="https://kb.shelly.cloud/knowledge-base/shelly-1?ref=blog.lchapman.dev" rel="noreferrer">Shelly1 v3</a> and a <a href="https://www.bft-automation.com/en_AU/product/deimos-bt-a400/?ref=blog.lchapman.dev">BFT Deimos BT A 400</a>, whereas I have a <a href="https://kb.shelly.cloud/knowledge-base/shelly-plus-1pm?ref=blog.lchapman.dev" rel="noreferrer">Shelly Plus 1PM</a> and a <a href="https://www.remotepro.com.au/products/copy-of-bft-genuine-merak-deimos-a600-ultra-control-board?ref=blog.lchapman.dev" rel="noreferrer">BFT HQSC-D Deimos BT</a> aftermarket replacement</p>]]></description><link>https://blog.lchapman.dev/making-a-gate-smart/</link><guid isPermaLink="false">65d6cf20b21afe00018143a9</guid><dc:creator><![CDATA[Lachlan Chapman]]></dc:creator><pubDate>Sat, 20 Dec 2025 01:47:24 GMT</pubDate><content:encoded><![CDATA[<p>For this project I will (hopefully) use information from this post: <a href="https://www.aaron-powell.com/posts/2023-01-16-building-a-smart-home---part-7-motorised-gate/?ref=blog.lchapman.dev" rel="noreferrer">Building a Smart Home - Part 7 Motorised Gate by Aaron Powell</a></p><p>Aaron has a <a href="https://kb.shelly.cloud/knowledge-base/shelly-1?ref=blog.lchapman.dev" rel="noreferrer">Shelly1 v3</a> and a <a href="https://www.bft-automation.com/en_AU/product/deimos-bt-a400/?ref=blog.lchapman.dev">BFT Deimos BT A 400</a>, whereas I have a <a href="https://kb.shelly.cloud/knowledge-base/shelly-plus-1pm?ref=blog.lchapman.dev" rel="noreferrer">Shelly Plus 1PM</a> and a <a href="https://www.remotepro.com.au/products/copy-of-bft-genuine-merak-deimos-a600-ultra-control-board?ref=blog.lchapman.dev" rel="noreferrer">BFT HQSC-D Deimos BT</a> aftermarket replacement board, made to replace the &quot;Deimos BT UL&quot;, but it isn&apos;t clear to me which product that actually is - <a href="https://www.bft-automation.com/en_AU/family-detail/motors-for-sliding-gates-up-to-600-kg-with-magnetic-limit-switch-and-u-link-integration-deimos-ultra-bt-a/?ref=blog.lchapman.dev" rel="noreferrer">best guess here</a>.</p><p>The Shelly Plus 1PM has power measurement capability that I will not be using here, I bought it because it was in stock and the Shelly1 was not. The Plus variety also has overcurrent and overvoltage protections, bluetooth, and uses an ESP32 for comms rather than an ESP8266. A Shelly1 would have been fine for this project. The gate board was replaced with an aftermarket part for the same reason of availability.</p><blockquote>Author&apos;s note: the PM variant of the board was <strong><em>not </em></strong>capable of providing the solution I needed, and I bought a Shelly Plus 1.</blockquote><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/Shelly-Plus-1PM.jpg" width="332" height="300" loading="lazy" alt></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/Shelly-Plus-1.jpg" width="332" height="300" loading="lazy" alt></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Left/red: Shelly Plus 1PM. Right/blue: Shelly Plus 1</span></p></figcaption></figure><p>I&apos;ll start by understanding Aaron&apos;s solution, then modifying it to fit my hardware.</p><p>Aaron connected L to L (for DC that&apos;s ground) and N to N (positive for DC), O to 61 (START) and I to 60 (COM). Wires to I and L are red, N is black and O is white. I&apos;m not going to assume those colours are reliable indicators of much because Aaron does not address it. I&apos;m also not particularly well versed with EEE, this project being at the edge of my knowledge.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/02/firefox_JQwGyvcxW0.png" width="253" height="370" loading="lazy" alt></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/1-internal-schematics.png" width="310" height="290" loading="lazy" alt></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/02/msedge_GkW34ru8Nd.png" width="969" height="1181" loading="lazy" alt srcset="https://blog.lchapman.dev/content/images/size/w600/2024/02/msedge_GkW34ru8Nd.png 600w, https://blog.lchapman.dev/content/images/2024/02/msedge_GkW34ru8Nd.png 969w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Aaron&apos;s hardware: Shelly1 and Deimos BT A400</span></p></figcaption></figure><p>Here are the same diagrams for my hardware:</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/Plus-1-AC-wiring-diagram-20240528-135400.png" width="386" height="456" loading="lazy" alt></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/Plus-1-internal-schematics.png" width="300" height="300" loading="lazy" alt></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/02/msedge_qeMBjiLivx.png" width="327" height="1081" loading="lazy" alt></div></div></div><figcaption><p><span style="white-space: pre-wrap;">My hardware: Shelly Plus 1 and Deimos BT UL025</span></p></figcaption></figure><p>The Shelly device closes the circuit between I and O when told to, which can be used to close a circuit on the board to trigger the open/close sequence.</p><p>This particular gate is configured with a keypad and also an RF board for remotes. A sub-board controls the inputs from those and handles authentication, but ultimately it simply closes a circuit once authentication is confirmed. All I need to do is piggy back the Shelly onto this circuit in parallel.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/IMG_6744.jpg" width="2000" height="1500" loading="lazy" alt srcset="https://blog.lchapman.dev/content/images/size/w600/2024/10/IMG_6744.jpg 600w, https://blog.lchapman.dev/content/images/size/w1000/2024/10/IMG_6744.jpg 1000w, https://blog.lchapman.dev/content/images/size/w1600/2024/10/IMG_6744.jpg 1600w, https://blog.lchapman.dev/content/images/2024/10/IMG_6744.jpg 2016w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2024/10/IMG_7260.jpg" width="1512" height="2016" loading="lazy" alt srcset="https://blog.lchapman.dev/content/images/size/w600/2024/10/IMG_7260.jpg 600w, https://blog.lchapman.dev/content/images/size/w1000/2024/10/IMG_7260.jpg 1000w, https://blog.lchapman.dev/content/images/2024/10/IMG_7260.jpg 1512w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Board photos</span></p></figcaption></figure><p>The bottom left corner contains 31-34. Red wire to 34 and black to 33, with two grey wires coming out of 31/32.  The manual says 33/34 is &quot;single-phase power supply 120VAC, 60Hz&quot; with 31/32 labelled &quot;transformer primary circuit 120VAC&quot;. However, measuring the voltage 33/34 shows 240V. It&apos;s definitely mains power.</p><p>Joining 21 and 22 momentarily with a multimeter causes the gate to open or close, although not with the same behaviour as when the remote is used. The remote either stops motion or starts motion in the opposite direction to the previous direction of motion, but what I observed here was that a closed gate begins to open, gets paused, then continues to open rather than beginning to close. Once the gate is fully open, another signal will begin to shut it. </p><p>My first successful configuration used the 240VAC from 34/34 to power the Shelly Plus 1 (into L/N) and then used breadboard wires to connect I/O on the Shelly to 21/22 on the main board, which also gets input from... somewhere else, maybe the road-side keypad. The Shelly can be told to close the switch, and the gate does things. Simple and easy, but not very versatile.</p><p>The next level of user experience was to integrate it into HomeAssistant via the Shelly integration. It works, but doesn&apos;t feed back any information about the position of the gate. You can send the signal whenever you like, but unless you are watching a camera feed, you can&apos;t be sure what is happening. So I set up a camera. I run Frigate already so that was fairly simple.</p><p>Having to open HomeAssistant and navigate to the Gate page is good, but there are a few things that are even better: widgets, and CarPlay.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2025/12/IMG_1750.PNG" width="1170" height="580" loading="lazy" alt srcset="https://blog.lchapman.dev/content/images/size/w600/2025/12/IMG_1750.PNG 600w, https://blog.lchapman.dev/content/images/size/w1000/2025/12/IMG_1750.PNG 1000w, https://blog.lchapman.dev/content/images/2025/12/IMG_1750.PNG 1170w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2025/12/Screenshot-2025-12-20-at-1.48.05-pm.png" width="924" height="554" loading="lazy" alt srcset="https://blog.lchapman.dev/content/images/size/w600/2025/12/Screenshot-2025-12-20-at-1.48.05-pm.png 600w, https://blog.lchapman.dev/content/images/2025/12/Screenshot-2025-12-20-at-1.48.05-pm.png 924w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Widgets make the experience as seamless as using a traditional gate remote</span></p></figcaption></figure><p>How does HomeAssistant know whether to do anything when the user activates the Shut Gate button? We must keep track of the position of the gate virtually with an estimate. I chose to keep a simple number, percentage of openness. </p><p>When the gate it shut, the user can activate the Open Gate script. That begins an automation that ticks the openness upward, firstly at speed, and for the final foot or so it moves slowly, as the gate creeps slowly to the end-stop.</p><p>The user can use Stop Gate at any time when the openness is not 0% or 100%, and send a signal to the gate which will halt movement, provided the gate is actually moving at that time - if you get the synchronisation wrong, and the gate has already stopped, it will begin moving again. We can mitigate this with a deasdzone</p><p>Shut Gate functions the same as Open Gate, but only triggers when the openness is 100%. Naturally there is a script in the background with a name like Gate Action and the Open Gate script just checks whether openness is 0% before calling Gate Action.</p><p>This all works fairly well! Multiple users can share the virtual gate controller without hassle. The CarPlay integration is fantastic, allowing the driver to operate the gate without touching their phone or rummaging around for a physical remote control. However, it does leave some to be desired.</p><p>Firstly, the virtual system does not play nice with any interactions it didn&apos;t initiate. Opened the gate with the keypad? Synchronisation is lost, until you close it with the keypad - if you do that. Same with using the remote controllers. I mitigate this with a slider in HomeAssistant that the user can set the openness with, and a dropdown to specify what will result from the next gate action. These were required in the background anyway, but exposing them was unfortunately necessary.</p><p>I have reached the limits of functionality using just a Shelly Plus 1. I&apos;m building a replacement using an ESP32 that will solve many of these issues. Stay tuned!</p>]]></content:encoded></item><item><title><![CDATA[Migrating to Proxmox]]></title><description><![CDATA[<p>I have a machine that runs Ubuntu. I want that OS to be running as a VM in Proxmox on the same machine with minimal downtime and without knowing that this process will work without problems. Here is my story.</p><p>This is not a guide. Don&apos;t expect to</p>]]></description><link>https://blog.lchapman.dev/migrating-to-proxmox/</link><guid isPermaLink="false">651a1f4a5cefdf0001de0218</guid><dc:creator><![CDATA[Lachlan Chapman]]></dc:creator><pubDate>Thu, 05 Oct 2023 05:14:52 GMT</pubDate><content:encoded><![CDATA[<p>I have a machine that runs Ubuntu. I want that OS to be running as a VM in Proxmox on the same machine with minimal downtime and without knowing that this process will work without problems. Here is my story.</p><p>This is not a guide. Don&apos;t expect to follow step by step. Read the whole thing first, then decide if any of it is right for you.</p><!--kg-card-begin: html--><h2>Part 1: Replicate</h2><!--kg-card-end: html--><p>NB: Part 1 is entirely redundant. It&apos;s what I did, but don&apos;t do it.</p><h3 id="step-1-procure-a-temporary-machine">Step 1: Procure a temporary machine</h3><p>Borrow it from a mate. Shoutout to Nathan. Alternatively, use anything you can, even if it&apos;s a bit old. Keep in mind that the hard drive will be wiped though.</p><h3 id="step-2-boot-both-machines-with-ubuntu-live-usbs">Step 2: Boot both machines with Ubuntu Live USBs</h3><p>But I want to use the same USB, so I boot &quot;to RAM&quot; using the toram flag in the bootloader. <a href="https://askubuntu.com/questions/829917/can-i-boot-a-live-usb-fully-to-ram-allowing-me-to-remove-the-disk?ref=blog.lchapman.dev">More details here.</a> If you don&apos;t have an Ubuntu Live USB, use <a href="https://rufus.ie/en/?ref=blog.lchapman.dev">Rufus</a> or <a href="https://etcher.balena.io/?ref=blog.lchapman.dev">Etcher</a> and <a href="https://ubuntu.com/download/desktop?ref=blog.lchapman.dev">get the ISO from here</a>.</p><h3 id="step-3-enable-ssh">Step 3: Enable SSH</h3><!--kg-card-begin: markdown--><ol>
<li>Install OpenSSH on both (<a href="https://ubuntu.com/server/docs/service-openssh?ref=blog.lchapman.dev">https://ubuntu.com/server/docs/service-openssh</a>)</li>
<li>Enable passwordless sudo on both (<a href="https://serverfault.com/questions/160581/how-to-setup-passwordless-sudo-on-linux?ref=blog.lchapman.dev">https://serverfault.com/questions/160581/how-to-setup-passwordless-sudo-on-linux</a>)</li>
<li>Set passwords for ubuntu account on both (<a href="https://www.cyberciti.biz/faq/change-a-user-password-in-ubuntu-linux-using-passwd/?ref=blog.lchapman.dev">https://www.cyberciti.biz/faq/change-a-user-password-in-ubuntu-linux-using-passwd/</a>)</li>
<li>Generate SSH key on main machine without a passphrase (<a href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key?ref=blog.lchapman.dev">https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key</a>)</li>
<li>Copy SSH public key to ubuntu@temp-machine using ssh-copy-id (<a href="https://www.ssh.com/academy/ssh/copy-id?ref=blog.lchapman.dev">https://www.ssh.com/academy/ssh/copy-id</a>)</li>
<li>Confirm that ubuntu@main-machine can connect to ubuntu@temp-machine using the passkey and no password</li>
</ol>
<!--kg-card-end: markdown--><h3 id="step-4-prepare-disk-for-copy">Step 4: Prepare disk for copy</h3><p>On the temporary machine, ensure a disk is prepared with enough space to receive the incoming clone data. Delete all partitions.</p><h3 id="step-5-install-dc3dd">Step 5: Install dc3dd</h3><!--kg-card-begin: markdown--><p>First you must enable the universe: <code>sudo add-apt-repository universe</code></p>
<p>Then install dc3dd: <code>sudo apt install dc3dd</code></p>
<!--kg-card-end: markdown--><h3 id="step-5-the-magic">Step 5: The Magic</h3><!--kg-card-begin: markdown--><pre><code class="language-bash">dc3dd if=/dev/sda | gzip | ssh ubuntu@temp-machine &quot;gzip -d | dd of=/dev/sda&quot;
</code></pre>
<p>To finetune, look into dc3dd parameters as well as gzip paramaters. They have been ignored here to keep it simple. This will take an hour per 100GB without tuning.</p>
<!--kg-card-end: markdown--><h3 id="step-6-reboot-temporary-machine-and-turn-off-main-machine">Step 6: Reboot temporary machine and turn off main machine</h3><p>That should be it. If the temporary machine doesn&apos;t work as a direct clone of the main machine, you&apos;ve lost nothing and can try again.</p><!--kg-card-begin: html--><h2>Part 2: Imaging</h2><!--kg-card-end: html--><p>You&apos;ve got your system working on the temporary machine. </p><h3 id="step-0-oh-no-that-was-a-waste-of-time">Step 0: Oh no, that was a waste of time</h3><p>At this point I realised I hadn&apos;t achieved anything useful because I had plans to load up the system first in VirtualBox, then in Proxmox as a VM on the temporary machine, then finally in Proxmox on the main machine.<br><br>Part 1 was entirely redundant.</p><h3 id="step-1-clonezilla-live-usb">Step 1: Clonezilla Live USB</h3><p>Install <a href="https://clonezilla.org/downloads.php?ref=blog.lchapman.dev">Clonezilla (stable)</a> to a USB.</p><h3 id="step-2-boot-temporary-machine-from-clonezilla-usb">Step 2: Boot temporary machine from Clonezilla USB</h3><p>However it works on your BIOS. I mash DEL on boot until the UEFI BIOS shows and then click Boot Menu.</p><h3 id="step-3-find-somewhere-to-store-the-image-and-plug-it-in">Step 3: Find somewhere to store the image and plug it in</h3><p>I set up an ext4 partition ona portable hard drive for this, sized to greater than the size of the installation I&apos;ll be storing. The image will be compressed though, so my 100GB installation was less than 50GB on the removable disk and the 200GB partition was larger than it needed to be.</p><h3 id="step-4-follow-the-docs">Step 4: Follow the docs</h3><p><a href="https://clonezilla.org/show-live-doc-content.php?topic=clonezilla-live%2Fdoc%2F01_Save_disk_image&amp;ref=blog.lchapman.dev">Here&apos;s the page you want.</a> I know, not sexy, but the Clonezilla docs do come with pictures. The partition you choose as the image repository is the one you just prepared. I chose -z9p and &quot;Yes, check the saved image&quot;. Then say yes twice to the y/n prompts.</p><!--kg-card-begin: html--><h2>Part 3: VM Verification</h2><!--kg-card-end: html--><p>At this point I have an image and I want to see if I can restore it somewhere else. I have two options: install Proxmox on the temporary machine and load it into that, or start smaller and load it up on in a VM within Windows. I chose the latter. I&apos;ve used VirtualBox a fair bit and VMWare a lot less so I opted to use VirtualBox.</p><h3 id="step-1-create-a-new-vm">Step 1: Create a new VM</h3><p>Important settings: a disk with 120GB of space, a few cores of CPU, a few GB of RAM, and the Clonezilla ISO loaded into the virtual disk drive.</p><h3 id="step-2-plug-in-the-removable-storage-and-attach-it-to-the-vm">Step 2: Plug in the removable storage and attach it to the VM</h3><p>VM Settings &gt; USB &gt; USB Device Filters, then add a filter for the relevant device. Mine was Seagate Expansion HDD [0001].</p><h3 id="step-3-boot-up-and-follow-the-docs">Step 3: Boot up and follow the docs</h3><p><a href="https://clonezilla.org/fine-print-live-doc.php?path=clonezilla-live%2Fdoc%2F02_Restore_disk_image&amp;ref=blog.lchapman.dev">Here&apos;s the page you want.</a> Basically the same process but choose restoredisk instead of savedisk.</p><h3 id="step-4-reboot-and-voila">Step 4: Reboot and voila!</h3><p>That&apos;s it, your system is loaded in a VM.</p><h2 id="part-4-proxmox">Part 4: Proxmox</h2><p>I know the cloned image is good. Now I want to experiment with Proxmox setup before installing for real on the main machine, so I&apos;ll install it on the temporary machine first.</p><h3 id="step-1-burn-a-proxmox-live-usb">Step 1: Burn a Proxmox Live USB</h3><p>Same as above, but with a <a href="https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso?ref=blog.lchapman.dev">Proxmox ISO</a>.</p><h3 id="step-2-install-proxmox">Step 2: Install Proxmox</h3><p>Boot the temporary machine using the Proxmox USB. Follow the installation process.</p><p>Don&apos;t expect it to work with wifi, just set it up on ethernet. Trust me. Run cables if you must.</p><h3 id="step-3-new-vm-who-dis">Step 3: New VM who dis</h3><p>First access the local (pve) section within pve within Datacenter. Open the ISO Images section and upload the Clonezilla ISO to it. </p><p>Make a new VM. It will need an ID number and 100 seems fine. Give it a name too. In the OS tab tell it to use the Clonezilla ISO image. in the System tab change the BIOS from SeaBIOS to OVMF (UEFI). A new drive will be suggested for you, scsi0, which is great but it needs more than 32GB so increase that. CPU settings matter too, 1 core will be very slow. I chose 4 cores and didn&apos;t touch the other settings because I haven&apos;t read about them yet. Likewise with memory, more than 2GB, I went with 10. Start it up!</p><h3 id="step-4-access-the-vm">Step 4: Access the VM</h3><p>Navigate to the Console section of the VM&apos;s interface. This is your screen. Your keyboard and mouse work here. Magic. Modern web browsers really can do anything.</p><h3 id="step-5-give-the-vm-access-to-the-removable-storage">Step 5: Give the VM access to the removable storage</h3><p><a href="https://dannyda.com/2020/08/26/how-to-passthrough-hdd-ssd-physical-disks-to-vm-on-proxmox-vepve/?ref=blog.lchapman.dev">I learned from this post.</a> Find the device with lshw, then find that in /dev/disk/by-id/. The main command, for reference:</p><!--kg-card-begin: html--><pre><code>qm set 100 -scsi5 /dev/disk/by-id/ata-xxxxxxxxx-xxxxx_xxx</code></pre><!--kg-card-end: html--><h3 id="step-6-same-as-part-3-step-3">Step 6: Same as Part 3 Step 3</h3><p>Boot the VM and use Clonezilla to restore the image from the removable storage onto the virtual disk.</p><h3 id="step-7-reboot-and-voila">Step 7: Reboot and voila!</h3><p>Your VM now lives within Proxmox. Very cool. Nice work.</p><h2 id="part-5-permanent">Part 5: Permanent</h2><p>The only thing left to do is to move the VM onto the main machine to live there permanently. Until now we&apos;ve been avoiding making any changes to the main machine but I feel safe in making irreversible changes since I have verified that my system has been imaged properly by restoring it twice.</p><h2 id="step-1-install-proxmox-for-real-this-time">Step 1: Install Proxmox for real this time</h2><p>Repeat step 2 of part 4, this time on the main machine.</p><h3 id="step-2-migrate-the-vm-from-temporary-proxmox-to-permanent-proxmox">Step 2: Migrate the VM from temporary Proxmox to permanent Proxmox</h3><p>There&apos;s a whole process to go through here. <a href="https://forum.proxmox.com/threads/framework-for-remote-migration-to-cluster-external-proxmox-ve-hosts.118444/?ref=blog.lchapman.dev">I started with this thread.</a></p><p>On the main machine, find the Permissions section in Datacenter and add a new API Token. Assign it to the root user and make sure Privilege separation is unticked. Give the token an ID, such as migration. Copy the token somewhere safe, you&apos;ll need this soon.</p><!--kg-card-begin: markdown--><p>SSH into the main machine:</p>
<p><code>ssh root@whatever_IP_address_you_set_it_up_with</code></p>
<p>Set up a temporary variable using your token. In this example, migration is the token ID - make sure this matches yours:</p>
<p><code>export APITOKEN=&apos;PVEAPIToken=root@pam!migration=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&apos;</code></p>
<p>Also export that IP address from before:</p>
<p><code>export HOSTNAME=&apos;192.168...&apos;</code></p>
<p>The command then looks like this:</p>
<p><code>qm remote-migrate 100 100 apitoken=${APITOKEN},host=${HOSTNAME} --target-bridge vmbr0 --target-storage local-lvm</code></p>
<p>If, like me, you get a response about a fingerprint not being verified, do the following:</p>
<p>First, copy the fingerprint into a variable:</p>
<p><code>export FINGERPRINT=&apos;36:8E:76:D0...&apos;</code></p>
<p>Then include it in the migration command:</p>
<p><code>qm remote-migrate 100 100 apitoken=${APITOKEN},host=${HOSTNAME},fingerprint=${FINGERPRINT} --target-bridge vmbr0 --target-storage local-lvm</code></p>
<!--kg-card-end: markdown--><p>Running this process took a while for me. I could see in the temporary Proxmox a status of &quot;config locked (migrate)&quot; on the VM and also a new VM appeared in the permanent Proxmox with status &quot;config locked (create)&quot;.</p><p>There is a way to track the progress of the migration: on the main machine, view the local-lvm store Summary for the Usage graph. I can see that the usage is 88GB on the temporary Proxmox local-lvm store so I expect the process will complete at a similar number. </p><h2 id="part-6-epilogue">Part 6: Epilogue</h2><p>The migration worked. The new VM on the permanent host has no status but the old one seems to have retained the lock. That doesn&apos;t matter to me, I&apos;ll be deleting it now anyway.</p><p>If you skip parts 1 and 3 and the redundant bits in part 4, you&apos;re left with a process that isn&apos;t too complicated or time consuming.</p>]]></content:encoded></item><item><title><![CDATA[Monitoring with the TIG Stack]]></title><description><![CDATA[<p>One day I&apos;ll implement Prometheus (a more free and open software) in place of InfluxDB but I haven&apos;t had the opportunity yet, so today we talk about the TIG stack:</p><!--kg-card-begin: markdown--><ul>
<li><strong>Telegraf</strong> records data about the state of a machine, and sends it to</li>
<li><strong>InfluxDB</strong> which stores</li></ul>]]></description><link>https://blog.lchapman.dev/monitoring-with-the-tig-stack/</link><guid isPermaLink="false">64647dc69707150001a86792</guid><dc:creator><![CDATA[Lachlan Chapman]]></dc:creator><pubDate>Thu, 18 May 2023 05:35:41 GMT</pubDate><media:content url="https://blog.lchapman.dev/content/images/2023/05/IMG_8206.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.lchapman.dev/content/images/2023/05/IMG_8206.jpg" alt="Monitoring with the TIG Stack"><p>One day I&apos;ll implement Prometheus (a more free and open software) in place of InfluxDB but I haven&apos;t had the opportunity yet, so today we talk about the TIG stack:</p><!--kg-card-begin: markdown--><ul>
<li><strong>Telegraf</strong> records data about the state of a machine, and sends it to</li>
<li><strong>InfluxDB</strong> which stores the data as time-series data, to be read by</li>
<li><strong>Grafana</strong> which displays real-time graphs in a dashboard</li>
</ul>
<!--kg-card-end: markdown--><p>Let&apos;s start with a dashboard screenshot:</p><figure class="kg-card kg-image-card"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_gFewOs7gG1-1.png" class="kg-image" alt="Monitoring with the TIG Stack" loading="lazy" width="893" height="800" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_gFewOs7gG1-1.png 600w, https://blog.lchapman.dev/content/images/2023/05/firefox_gFewOs7gG1-1.png 893w" sizes="(min-width: 720px) 720px"></figure><p>It never hurts to be able to have metrics for your various machines available at a glance, but there&apos;s so much more you can do than just imitate Task Manager. See <a href="https://gideonwolfe.com/posts/sysadmin/tig/intro/?ref=blog.lchapman.dev">this post</a> and <a href="https://gideonwolfe.com/posts/sysadmin/tig/advanced/?ref=blog.lchapman.dev">the followup</a> for a great writeup.</p><p>Some software issues are just hardware or system issues in disguise, so it pays to have easy access to information about system health across an organisation. Whether you need to find a bottleneck or just check that all machines are running or have network connectivity, when the issue arises, you&apos;ll be glad you set this up.</p><h2 id="deploy-it">Deploy It</h2><p>Without further ado, some configs:</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">version: &apos;2&apos;

services:

  influxdb:
    image: influxdb:alpine
    restart: unless-stopped
    ports:
      - &apos;12102:8086&apos;
    volumes:
      - ./influxdb/data:/var/lib/influxdb2:rw
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=${INFLUXDB_USERNAME}
      - DOCKER_INFLUXDB_INIT_PASSWORD=${INFLUXDB_PASSWORD}
      - DOCKER_INFLUXDB_INIT_ORG=${INFLUXDB_DEFAULT_ORG}
      - DOCKER_INFLUXDB_INIT_BUCKET=${INFLUXDB_PRIMARY_BUCKET}
      - DOCKER_INFLUXDB_INIT_RETENTION=${INFLUXDB_PRIMARY_BUCKET_RETENTION}
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${INFLUXDB_ADMIN_TOKEN}

  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    ports:
      - &apos;12101:3000&apos;
    volumes:
      - ./grafana/data:/var/lib/grafana
      - ./grafana/provisioning/:/etc/grafana/provisioning
    depends_on:
      - influxdb
    environment:
      - GF_SECURITY_ADMIN_USER=${GRAFANA_USERNAME}
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}</code></pre><figcaption>compose file</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-env">INFLUXDB_USERNAME=admin
INFLUXDB_PASSWORD=correct-horse-battery-staple
INFLUXDB_DEFAULT_ORG=lchapman.dev
INFLUXDB_PRIMARY_BUCKET=weekly
INFLUXDB_PRIMARY_BUCKET_RETENTION=7d
INFLUXDB_ADMIN_TOKEN=right-brumby-capacitor-fastener

GRAFANA_USERNAME=admin
GRAFANA_PASSWORD=running-out-of-ideas</code></pre><figcaption>.env</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-Caddyfile">...

# Grafana
http://grafana.lchapman.dev {
        reverse_proxy localhost:12101
}

# Influx
http://influx.lchapman.dev {
        reverse_proxy localhost:12102
}

...</code></pre><figcaption>Caddyfile</figcaption></figure><!--kg-card-begin: markdown--><p>Some notes:</p>
<ol>
<li>See <a href="https://blog.lchapman.dev/traffic-management-with-caddy/">my post about Caddy</a> for more details about the Caddyfile.</li>
<li>I use the default org for my personal infrastructure, then any specific infrastructure (for a client, for example) will use a separate org (i.e. client1.lchapman.dev).</li>
<li>The choice of 7 days of retention is arbitrary, choose a time interval that suits your needs.</li>
<li>The InfluxDB volume needs to point to the influxdb2 folder. The compose file I worked from originally had been adapted from InfluxDB v1 (where the folder was just influxdb) and therefore my first couple of trial runs had no persistence.</li>
</ol>
<!--kg-card-end: markdown--><p>So that&apos;s the database and the dashboard set up in containers, but where does the data come from? Initially I had Telegraf running in the compose stack but the system stats that were coming from it were related to the container rather than the system, so it becomes one of the rare softwares I install using more traditional means. <a href="https://docs.influxdata.com/telegraf/latest/install/?ref=blog.lchapman.dev">See here for details</a>. After that, deploy your config and off you go.</p><figure class="kg-card kg-code-card"><pre><code class="language-sh"># For InfluxDB OSS 2:
[[outputs.influxdb_v2]]
  urls = [&quot;https://influx.lchapman.dev&quot;]
  token = &quot;your-token-here-but-don&apos;t-forget-to-make-it-first&quot;
  organization = &quot;lchapman.dev&quot;
  bucket = &quot;weekly&quot;

[agent]
  interval = &quot;10s&quot;
  hostname = &quot;gateway&quot;

# Inputs

[[inputs.cpu]]
  percpu = true
  totalcpu = true
  collect_cpu_time = false
  report_active = false
[[inputs.disk]]
  ignore_fs = [&quot;tmpfs&quot;, &quot;devtmpfs&quot;, &quot;devfs&quot;, &quot;iso9660&quot;, &quot;overlay&quot;, &quot;aufs&quot;, &quot;squashfs&quot;]
[[inputs.diskio]]
[[inputs.mem]]
[[inputs.net]]
  interfaces = [&quot;eth*&quot;, &quot;tun0&quot;, &quot;lo&quot;]
[[inputs.processes]]
[[inputs.swap]]
[[inputs.system]]</code></pre><figcaption>/etc/telegraf/telegraf.conf</figcaption></figure><!--kg-card-begin: markdown--><p>More notes:</p>
<ol>
<li>The URL represents the route to my InfluxDB. If you&apos;re running multiple databases you can specify multiple destinations. Neat.</li>
<li>The token doesn&apos;t exist yet. Go to Influx &gt; Load Data &gt; API Tokens &gt; Generate API Token (Custom API Token). Name it something descriptive like weekly_Telegraf_gateway, give it write access to the weekly bucket and click Generate.</li>
<li>The outputs.influxdb_v2 section will be consistent across all of your machines, but the agent section will not. Set the hostname per machine.</li>
<li>The inputs section here is populated with a template I found useful, but do check out the <a href="https://docs.influxdata.com/telegraf/latest/get-started/?ref=blog.lchapman.dev">Telegraf docs</a> to get the data you care about. There&apos;s a whole ecosystem of plugins. Seriously, check it out.</li>
</ol>
<!--kg-card-end: markdown--><h3 id="display-it">Display It</h3><p>We have a database that is being populated: great! Let&apos;s see it then. InfluxDBv2 uses its own language to query data called Flux. Flux lang is flexible and readable and while not everybody is happy that it replaced the SQL-like language used in InfluxDBv1, it&apos;s fine. Having to learn yet another language is fine. It&apos;s fine.</p><p>Use the Data Explorer within your Influx web interface to start crafting your queries, then click Script Editor to access the actual Flux query.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_kb5mhvoW66.png" width="1916" height="1917" loading="lazy" alt="Monitoring with the TIG Stack" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_kb5mhvoW66.png 600w, https://blog.lchapman.dev/content/images/size/w1000/2023/05/firefox_kb5mhvoW66.png 1000w, https://blog.lchapman.dev/content/images/size/w1600/2023/05/firefox_kb5mhvoW66.png 1600w, https://blog.lchapman.dev/content/images/2023/05/firefox_kb5mhvoW66.png 1916w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_Qik6Y80RXi.png" width="1916" height="1917" loading="lazy" alt="Monitoring with the TIG Stack" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_Qik6Y80RXi.png 600w, https://blog.lchapman.dev/content/images/size/w1000/2023/05/firefox_Qik6Y80RXi.png 1000w, https://blog.lchapman.dev/content/images/size/w1600/2023/05/firefox_Qik6Y80RXi.png 1600w, https://blog.lchapman.dev/content/images/2023/05/firefox_Qik6Y80RXi.png 1916w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption>Query Builder vs Script Editor</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-Flux">from(bucket: &quot;weekly&quot;)
  |&gt; range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |&gt; filter(fn: (r) =&gt; r[&quot;_measurement&quot;] == &quot;mem&quot;)
  |&gt; filter(fn: (r) =&gt; r[&quot;_field&quot;] == &quot;used&quot;)
  |&gt; filter(fn: (r) =&gt; r[&quot;host&quot;] == &quot;gateway&quot;)
  |&gt; aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
  |&gt; yield(name: &quot;mean&quot;)</code></pre><figcaption>Example query</figcaption></figure><p>It&apos;s all pretty easy to follow, except maybe the v variable which is supplied by the viewer, in this case the Data Explorer but later it will be Grafana. The highest level of data organisation is in a &quot;measurement&quot; which contains some &quot;field&quot;. Past that there are tags, such as the host tag here which is set by Telegraf as per our config. </p><p>The aggregateWindow function is worth explaining. Without it, the request could return an arbitrarily large number of datapoints, and while I do love high levels of precision, there&apos;s a tradeoff to be made regarding the load placed on the database, especially if your dashboard is set to update every 10 seconds. The value passed to <em>every</em> could be a constant, like 1m (1 minute), or it can be dynamic, like v.windowPeriod: a 24 hour window produces 2m period, 7 days is 15m, 2 hours is 10s and anything less is actually doing nothing because my data collection interval is 10 seconds anyway. Set createEmpty to true if your machine ever powers off; there&apos;s not much reason to leave it false regardless. For my graphs of CPU usage I set the aggregate function to max rather than mean because I&apos;m looking for bottlenecks and mean hides them.</p><p>A new Grafana instance may require you to head to the Connections menu and configure the InfluxDB connection. Use the Flux query language rather than InfluxQL, it&apos;s the one in active development. I found that the URL field works fine as either of <a href="http://influxdb:8086/?ref=blog.lchapman.dev">http://influxdb:8086</a> or <a href="https://influx.lchapman.dev/?ref=blog.lchapman.dev">https://influx.lchapman.dev/</a>, with the former being preferable because traffic need not exit the container stack in this case. Turn off the basic auth toggle. Set the organization and default bucket as per your environmental variables. Min time interval of 10s is correct here because that&apos;s my minimum data write period.</p><p>The token I supply to Grafana doesn&apos;t exist yet either. I create a read-only token for the weekly bucket called weekly_Grafana. Paste it in, click save &amp; test and you should see a green tick and the message &quot;datasource is working. 1 buckets found&quot;.</p><p>Now you&apos;ll want to go to Dashboards &gt; New Dashboard &gt; Add vizualization, paste the query from Influx into the A query, then click somewhere outside the query textbox. Grafana will see your changes and load the data. If it looks good, click Save. In my case, I&apos;ll want to fiddle with the options a bit. Set the unit to bytes (SI) in Standard Options and while I&apos;m there I&apos;ll set a min and max. A proper title helps too.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_M0SRgHC9UO.png" width="1234" height="951" loading="lazy" alt="Monitoring with the TIG Stack" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_M0SRgHC9UO.png 600w, https://blog.lchapman.dev/content/images/size/w1000/2023/05/firefox_M0SRgHC9UO.png 1000w, https://blog.lchapman.dev/content/images/2023/05/firefox_M0SRgHC9UO.png 1234w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_eXkiFX2kP9.png" width="1082" height="848" loading="lazy" alt="Monitoring with the TIG Stack" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_eXkiFX2kP9.png 600w, https://blog.lchapman.dev/content/images/size/w1000/2023/05/firefox_eXkiFX2kP9.png 1000w, https://blog.lchapman.dev/content/images/2023/05/firefox_eXkiFX2kP9.png 1082w" sizes="(min-width: 720px) 720px"></div></div></div></figure><p>One last lesson before I wrap this up: computed series names. If you&apos;re doing a per-core CPU graph, you&apos;ll immediately see an issue:</p><figure class="kg-card kg-image-card"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_gkFie3HQHi.png" class="kg-image" alt="Monitoring with the TIG Stack" loading="lazy" width="856" height="761" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_gkFie3HQHi.png 600w, https://blog.lchapman.dev/content/images/2023/05/firefox_gkFie3HQHi.png 856w" sizes="(min-width: 720px) 720px"></figure><p>Note the map function to apply a transform to every data point, converting a measurement of core idleness to core busy-ness. Also note the legend being awful due to series names being just daft. I suppose it&apos;s a sensible default behaviour but it falls apart in this particular use case. Here&apos;s our silver bullet:</p><pre><code class="language-Flux">  |&gt; map(fn: (r) =&gt; ({ _value:r._value, _time:r._time, _field:r.cpu }))</code></pre><p>Time and value are passed through but the field name is overwritten by just the value of the cpu tag. Handy! I suppose I could write a function to strip the &quot;cpu&quot; part from &quot;cpu7&quot; but... I&apos;m not going to. The result:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.lchapman.dev/content/images/2023/05/firefox_DjHAoY2uhW.png" class="kg-image" alt="Monitoring with the TIG Stack" loading="lazy" width="857" height="753" srcset="https://blog.lchapman.dev/content/images/size/w600/2023/05/firefox_DjHAoY2uhW.png 600w, https://blog.lchapman.dev/content/images/2023/05/firefox_DjHAoY2uhW.png 857w" sizes="(min-width: 720px) 720px"><figcaption>Close enough!</figcaption></figure><p>The eagle-eyed amongst you will notice a lack of optimisation: why not move the first map function after the aggregateWindow, reducing the number of rows the map operates on? Doing so reduces the query time from 500ms to 350ms. Moving the minus operation into the second map and removing the first altogether brings it down to 280ms, for a total reduction of processing time of 45% - not bad. My sample sizes for this data were ~2, hardly rigorous, but there really is a measurable change. Combining the filter calls from 4 to 1 (a series of and statements) however does not elicit any performance gains worth noting, and reduces readability to miserable lows.</p><p>There&apos;s plenty more I&apos;ll learn about Flux-lang as I go, but for now, I don&apos;t mind it. It seems to have the tools I need. I&apos;ll try Prometheus one day, but not today.</p>]]></content:encoded></item><item><title><![CDATA[Traffic Management with Caddy]]></title><description><![CDATA[<p>A quick refresher on the gateway model I employ: one machine is accessible from the internet, minimising the exposed surface area of my network. This machine is the gateway and serves all requests. From a DNS perspective, all of my subdomains are CNAME records pointing to the gateway.</p><p>Nginx and</p>]]></description><link>https://blog.lchapman.dev/traffic-management-with-caddy/</link><guid isPermaLink="false">645ce3971b701c000160411a</guid><dc:creator><![CDATA[Lachlan Chapman]]></dc:creator><pubDate>Thu, 11 May 2023 15:10:11 GMT</pubDate><media:content url="https://blog.lchapman.dev/content/images/2023/05/traffic.JPG" medium="image"/><content:encoded><![CDATA[<img src="https://blog.lchapman.dev/content/images/2023/05/traffic.JPG" alt="Traffic Management with Caddy"><p>A quick refresher on the gateway model I employ: one machine is accessible from the internet, minimising the exposed surface area of my network. This machine is the gateway and serves all requests. From a DNS perspective, all of my subdomains are CNAME records pointing to the gateway.</p><p>Nginx and Apache are the main names in the web server game, and while I&apos;ve used them both, my eye was drawn to Caddy. It&apos;s sleek and modern and addresses most of my gripes with the older players. I quickly fell in love with Caddy and one day it will be my downfall, but until then, I&apos;ll use it as my hammer.</p><p>I began with a simple configuration and have since found myself running multiple layers of Caddy, but I&apos;ll get to that further down.</p><p>As I run Debian-based operating systems most of the time, <a href="https://caddyserver.com/docs/install?ref=blog.lchapman.dev#debian-ubuntu-raspbian">I followed these instructions to install the stable release</a>. If you&apos;re not familiar with systemd or just want a refresher, there&apos;s a <a href="https://caddyserver.com/docs/running?ref=blog.lchapman.dev#using-the-service">page in the docs</a> that explains how to manage it.</p><!--kg-card-begin: html--><p>Configuring Caddy is done by loading a &quot;Caddyfile&quot; into the running service, or at process launch with <code>caddy run --config /path/to/Caddyfile</code>. It&apos;s easy enough to make a formatting mistake, so I operate a &quot;working document&quot; that resides in my home directory. Caddy provides a function to format the document into a valid Caddyfile which is invoked as <code>caddy fmt working &gt; Caddyfile</code>. Copy that to /etc/caddy and reload and you&apos;re good to go.</p><!--kg-card-end: html--><figure class="kg-card kg-code-card"><pre><code class="language-bash">#!/bin/bash

caddy fmt working &gt; Caddyfile &amp;&amp; \
sudo cp Caddyfile /etc/caddy/ &amp;&amp; \
rm Caddyfile &amp;&amp; \
caddy reload --config /etc/caddy/Caddyfile</code></pre><figcaption>update_Caddyfile_and_reload.sh</figcaption></figure><p>So what does my working config look like?</p><figure class="kg-card kg-code-card"><pre><code class="language-Caddyfile">www.lchapman.dev, lchapman.dev {
        # Not doing a website yet, redirect to the blog
        redir https://blog.lchapman.dev
}

blog.lchapman.dev {
        reverse_proxy 10.8.0.2
}

# Other services in similar formats
# ...</code></pre><figcaption>working</figcaption></figure><p>You might find you don&apos;t need to use fmt as part of your workflow if you don&apos;t make mistakes, but I move fast and make plenty.</p><p>Now we get to the part where Caddy is brought into line with the likes of Nginx and Apache: the Caddyfile is just a shorthand for the real config. Use adapt function to parse your Caddyfile and return the result, and you&apos;ll be greeted with this:</p><pre><code class="language-json">{
  &quot;apps&quot;: {
    &quot;http&quot;: {
      &quot;servers&quot;: {
        &quot;srv0&quot;: {
          &quot;listen&quot;: [&quot;:443&quot;],
          &quot;routes&quot;: [{
            &quot;match&quot;: [{
              &quot;host&quot;: [&quot;blog.lchapman.dev&quot;]
            }],
            &quot;handle&quot;: [{
              &quot;handler&quot;: &quot;subroute&quot;,
              &quot;routes&quot;: [{
                &quot;handle&quot;: [{
                  &quot;handler&quot;: &quot;reverse_proxy&quot;,
                  &quot;upstreams&quot;: [{
                    &quot;dial&quot;: &quot;10.8.0.2:80&quot;
                  }]
                }]
              }]
            }],
            &quot;terminal&quot;: true
          }, {
            &quot;match&quot;: [{
              &quot;host&quot;: [&quot;www.lchapman.dev&quot;, &quot;lchapman.dev&quot;]
            }],
            &quot;handle&quot;: [{
              &quot;handler&quot;: &quot;subroute&quot;,
              &quot;routes&quot;: [{
                &quot;handle&quot;: [{
                  &quot;handler&quot;: &quot;static_response&quot;,
                  &quot;headers&quot;: {
                    &quot;Location&quot;: [&quot;https://blog.lchapman.dev&quot;]
                  },
                  &quot;status_code&quot;: 302
                }]
              }]
            }],
            &quot;terminal&quot;: true
          }]
        }
      }
    }
  }
}</code></pre><p>It is verbose but that&apos;s standard for web server configuration files. Caddy allows us to avoid maintaining this JSON behemoth by hand, and I am very grateful for it.</p><p>Note how line 6 shows that Caddy is listening to port 443. By failing to specify http or https in the Caddyfile, Caddy defaults to TLS via LetsEncrypt, handling the process of requesting, serving and updating HTTPS certificates behind the scenes. TLS is terminated at the gateway and all traffic in the lchapman.dev private network is passed around via HTTP, with the eventual response returned and encrypted by the gateway on the way out into the WAN.</p><p>This is a great configuration - it&apos;s simple, easy, and no-maintenance. But what if I don&apos;t want to terminate TLS at the gateway? Say, perhaps, some software running downstream expects encrypted traffic. Say, perhaps, the correct course of action is to modify an Nginx config in a container to expect plaintext traffic; but I don&apos;t feel like doing that, so what to do?</p><p>Caddy does offer a solution! Alas, it&apos;s not so simple. I&apos;ll do it anyway though because Caddy is my hammer and I spy a sghiny new nail.</p><p>If you refer above to the JSON config you&apos;ll see the &quot;apps&quot; key at the top level and &quot;http&quot; as its first and only child. The <a href="https://caddyserver.com/docs/json/apps/http/?ref=blog.lchapman.dev">http app</a> is great but it has limits and unfortunately we&apos;re looking for a feature that is out of scope. Enter <a href="https://caddyserver.com/docs/json/apps/layer4/?ref=blog.lchapman.dev">layer4</a>. Layer4, or <a href="https://github.com/mholt/caddy-l4?ref=blog.lchapman.dev">caddy-l4, or Project Conncept</a>, is an app written by Matt Holt (who is the major contributor to and sponsor of Caddy) to extend Caddy&apos;s capabilities to <a href="https://osi-model.com/transport-layer/?ref=blog.lchapman.dev">layer 4</a>, where Caddy&apos;s base functionality mainly deals with <a href="https://osi-model.com/application-layer/?ref=blog.lchapman.dev">layer 7</a>. I expect layer4 will one day be shipped with Caddy proper, but for now we must build it ourselves.</p><p>Enter <a href="https://github.com/caddyserver/xcaddy?ref=blog.lchapman.dev">xcaddy - Custom Caddy Builder</a>. They really have thought of everything. Install it with go or grab it with apt. Build your own caddy with it. It&apos;s too easy.</p><!--kg-card-begin: html--><p>
    <code>$ xcaddy build latest \
    --output caddy-l4 \
    --with github.com/mholt/caddy-l4</code>
</p><!--kg-card-end: html--><p>I opted to name the binary differently to the binary installed with apt and copied it to /usr/bin alongside apt-caddy. I also copied the service files in /lib/systemd/system (caddy.service and caddy-api.service) and made new ones for caddy-l4. </p><figure class="kg-card kg-code-card"><pre><code class="language-systemd">[Unit]
Description=Caddy Layer4
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy-l4 run --environ --config /etc/caddy/config.json
ExecReload=/usr/bin/caddy-l4 reload --config /etc/caddy/config.json --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateDevices=yes
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target</code></pre><figcaption>caddy-l4.service</figcaption></figure><p>The idea is that caddy-l4 listens to port 443, follows a minimal amount of rules, and delegates most of the work back to regular caddy which now listens to port 8443. Layer4 does not yet have Caddyfile support so we&apos;ll have to implement the config in JSON. I built on the examples on the Github repo and had help from the community server on Discord and came up with this:</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{
  &quot;apps&quot;: {
    &quot;layer4&quot;: {
      &quot;servers&quot;: {
        &quot;first_layer&quot;: {
          &quot;listen&quot;: [
            &quot;:443&quot;,
            &quot;:80&quot;
          ],
          &quot;routes&quot;: [{
              &quot;match&quot;: [{
                  &quot;tls&quot;: {
                    &quot;sni&quot;: [
                      &quot;nginx.service.lchapman.dev&quot;
              ]}}],
              &quot;handle&quot;: [{
                  &quot;handler&quot;: &quot;proxy&quot;,
                  &quot;upstreams&quot;: [{
                      &quot;dial&quot;: [
                        &quot;10.8.0.2:443&quot;
              ]}]}]},
            {
              &quot;match&quot;: [{
                  &quot;tls&quot;: {}
              }],
              &quot;handle&quot;: [{
                  &quot;handler&quot;: &quot;proxy&quot;,
                  &quot;upstreams&quot;: [{
                      &quot;dial&quot;: [
                        &quot;localhost:8443&quot;
            ]}]}]},
            {
              &quot;match&quot;: [{
                  &quot;http&quot;: []
              }],
              &quot;handle&quot;: [{
                  &quot;handler&quot;: &quot;proxy&quot;,
                  &quot;upstreams&quot;: [{
                      &quot;dial&quot;: [
                        &quot;localhost:8080&quot;
  ]}]}]}]}}}}
}</code></pre><figcaption>config.json</figcaption></figure><p>Please excuse the formatting atrocities committed here, I wanted to save you from endless scrolling and keep the meat of the content on one screen.</p><p>If the request is HTTPS with a particular SNI (<a href="https://en.wikipedia.org/wiki/Fully_qualified_domain_name?ref=blog.lchapman.dev">FQDN</a>), we match it and forward it to the next layer of Caddy with TLS intact. For everything else, if it&apos;s on port 80 then forward to port 8080 and 443 to 8443. Port 80 is interesting here because I won&apos;t receive web traffic on it via my domain because .dev domains implement <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security?ref=blog.lchapman.dev">HSTS</a> at the domain level, meaning web browsers refuse to send an insecure HTTP request to lchapman.dev at all. This is good, because developers should be expected to lead the way with security. Why open port 80 at all then, and why bother forwarding it? Caddy uses <a href="https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment?ref=blog.lchapman.dev">ACME</a> to manage certificates and the process to obtain or renew a certificate requires port 80 - it&apos;s difficult to bootstrap TLS if TLS is required to do so. Our outer caddy-l4 instance doesn&apos;t manage any certificates so we forward all port 80 traffic to the inner caddy, which does manage the certificates.</p><p>A slight modification to the original Caddyfile is required to make this work. Add the following to the top:</p><pre><code class="language-Caddyfile">{
	admin 0.0.0.0:2020
	http_port 8080
	https_port 8443
}</code></pre><p>The admin line prevents the two caddy processes from trying to bind to the same port (2019) to run their API services. The other two lines have hopefully been explained ahead of time.</p><p>That&apos;s the full explanation of the gateway&apos;s multi-Caddy configuration. To provide the full picture, I&apos;ll give a brief rundown of the third and final instance of Caddy which resides at 10.8.0.2. It runs a Caddyfile but it specifies http or https rather than omitting them.</p><figure class="kg-card kg-code-card"><pre><code class="language-Caddyfile"># ______           __     __         ___ __ __        
#|      |.---.-.--|  |.--|  |.--.--.&apos;  _|__|  |.-----.
#|   ---||  _  |  _  ||  _  ||  |  |   _|  |  ||  -__|
#|______||___._|_____||_____||___  |__| |__|__||_____|
#                            |_____|

#	port	subdomain	service
#	2368	blog		Ghost
#	3000	nginx.service	TLS-only service
#	...	...		...

# Blog
http://blog.lchapman.dev {
	reverse_proxy localhost:2368 {
		trusted_proxies private_ranges
	}
}

# TLS-only service
https://nginx.service.lchapman.dev {
	reverse_proxy https://localhost:3000 {
		trusted_proxies private_ranges
		transport http {
			tls_server_name nginx.service.lchapman.dev
		}
	}
}

# ...</code></pre><figcaption>Caddyfile (10.8.0.2)</figcaption></figure><p>Some services work without specifying trusted proxies, some don&apos;t. It never hurts to include the directive though, so go nuts with it. Ghost requires it. The TLS-only service also requires us to specify the tls_server_name otherwise it will send a request to localhost which will be received by localhost but the whole problem we&apos;re solving is that this specific Nginx setup is expecting all traffic to be addressed to nginx.service.lchapman.dev and be encrypted with the proper certificate. We overwrite it here and life is good.</p><p>That&apos;s all there is to it. Let&apos;s say I want to spin up a new container with a new service: <strong>hello-world.</strong> <a href="https://hub.docker.com/_/hello-world?ref=blog.lchapman.dev">(Dockerhub link)</a></p><!--kg-card-begin: markdown--><p>It&apos;s pretty easy:</p>
<ol>
<li>Set up a compose file with a hello-world service using the hello-world image and expose it on port 3001</li>
<li><code>docker compose up -d</code></li>
<li>Set up a DNS entry with type CNAME, hostname hello.lchapman.dev, destination gateway.lchapman.dev.</li>
<li>Add an entry to the gateway working config with matcher hello.lchapman.dev and reverse_proxy to 10.8.0.2. Reload with reload.sh.</li>
<li>Add an entry to the 10.8.0.2 working config with matcher <a href="http://hello.lchapman.dev/?ref=blog.lchapman.dev">http://hello.lchapman.dev</a> and reverse_proxy to <a href="http://localhost:3001/?ref=blog.lchapman.dev">http://localhost:3001</a>. Reload with reload.sh.</li>
<li>Send a request to <a href="https://hello.lchapman.dev/?ref=blog.lchapman.dev">https://hello.lchapman.dev/</a> and receive an error because Caddy doesn&apos;t have a certificate for that domain yet</li>
<li>Come back a minute later, refresh and see hello world</li>
</ol>
<!--kg-card-end: markdown--><p>The thought of going back to Nginx for this sends shivers down my spine.</p><h3 id="bonus-content-caddy-in-docker">BONUS CONTENT: Caddy in Docker</h3><p>Yes. Do it. I almost did do it, especially when I was in the process of deploying caddy-l4, but I ultimately didn&apos;t because my gateway is a tiny VM with minimum specs (for minimum pricetag) and I didn&apos;t want to install Docker on it at all.</p><p>The way I see it, anything Nginx/Apache can do, Caddy can probably do in a way that is nicer to work with. Better? I&apos;ll let somebody with a lot more knowledge decide that. Caddy is nicer though, I&apos;ll die on that hill.</p>]]></content:encoded></item><item><title><![CDATA[Self Hosting Foundations]]></title><description><![CDATA[<p>I&apos;ve always had a passion for free and open source software. Naturally this has evolved into a self-hosting obsession. Some softwares that would otherwise be gated behind large upfront costs or subscription fees have perfectly serviceable free alternatives, at the cost of having to administer them yourself.</p><p>I</p>]]></description><link>https://blog.lchapman.dev/self-hosting-foundations/</link><guid isPermaLink="false">643faf271b701c000160403c</guid><dc:creator><![CDATA[Lachlan Chapman]]></dc:creator><pubDate>Wed, 19 Apr 2023 10:00:22 GMT</pubDate><media:content url="https://blog.lchapman.dev/content/images/2023/04/IMG_0722_edit.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.lchapman.dev/content/images/2023/04/IMG_0722_edit.jpg" alt="Self Hosting Foundations"><p>I&apos;ve always had a passion for free and open source software. Naturally this has evolved into a self-hosting obsession. Some softwares that would otherwise be gated behind large upfront costs or subscription fees have perfectly serviceable free alternatives, at the cost of having to administer them yourself.</p><p>I have a couple of machines running Ubuntu that function as servers. They live in a home network, and while one could forward ports and expose them to the WAN, I choose not to do that out of an abundance of caution. Instead I use what I call a &quot;gateway machine&quot;.</p><p>The gateway machine is actually a VM run on the cloud, specifically the cheapest one I could find that I liked. I&apos;ve used DigitalOcean as a cloud provider for work and found them reliable and reasonably priced, and their wide range of tutorials is also fantastic and has guided me through many processes. The gateway is a single core &quot;droplet&quot; instance with minimal memory and costs $4 USD per month.</p><p>The main function of the gateway is to have a static, public IP address that my domain (lchapman.dev) is pointed to. Ports are opened as needed, but I try to avoid exposing any more ports than are strictly necessary, so 443 is the main one. &quot;.DEV&quot; domains are HSTS domains (<a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security?ref=blog.lchapman.dev">HTTP Strict Transport Security</a>) which means the browser will reject any connections that aren&apos;t utilising TLS (<a href="https://en.wikipedia.org/wiki/Transport_Layer_Security?ref=blog.lchapman.dev">Transport Layer Security</a>), so all traffic must be properly encrypted. Encrypted web traffic conventially uses port 443.</p><p>Your best friend for encrypting web traffic and facilitating HTTPS is <a href="https://letsencrypt.org/?ref=blog.lchapman.dev">Let&apos;sEncrypt</a>, the free and open certificate authority. The method I learned first for achieving HTTPS traffic was using <a href="https://certbot.eff.org/?ref=blog.lchapman.dev">certbot</a> to generate certificates, then using Nginx to encrypt and serve content with those certificates. Nginx is as old as the hills and all powerful but there are modern alternatives that I find much more comfortable - Nginx is still the car you take to the track to race, but not my preference for a daily driver.</p><p><a href="https://caddyserver.com/?ref=blog.lchapman.dev">Caddy Server (specifically Caddy 2)</a> does everything I need and does it in a human-friendly way, so I&apos;m very happy to have discovered it.</p><p>Each VM or machine that I run that serves any kind of content runs Caddy which listens to incoming traffic and handles them based on rules. My gateway machine receives all incoming traffic and Caddy handles it all, routing it wherever it needs to go.</p><p>Here&apos;s a snippet from the gateway Caddyfile (config):</p><!--kg-card-begin: markdown--><pre><code>www.lchapman.dev, lchapman.dev {
        redir https://blog.lchapman.dev
}

blog.lchapman.dev {
        reverse_proxy 10.8.0.2
}
</code></pre>
<!--kg-card-end: markdown--><p>It&apos;s short, simple and readable. All traffic to www.lchapman.dev or lchapman.dev is redirected to blog.lchapman.dev. Requests to blog.lchapman.dev are passed to 10.8.0.2, a machine on a local network. Any further domains that need to be routed are done so with similar directives.</p><p>Using a reverse proxy to send traffic to 10.8.0.2 means that clients only know they are interacting with blog.lchapman.dev and nothing more, but the actual blog is hosted elsewhere. <a href="https://ghost.org/?ref=blog.lchapman.dev">Ghost</a> is the blog software and is hosted on a machine connected to a VPN.</p><p>The only software running on the gateway machine besides Caddy is an OpenVPN server. All machines that I host softwares on are connected as clients to that VPN. One of the benefits of this configuration are that I can pick up my machines and move them to a new location without having to reconfigure my network - they reconnect to the VPN and everything works as before. This flexibility is very handy for self-hosted machines.</p><p>All that remains to discuss here are the servers that host my softwares. Currently I don&apos;t run them as a cluster but I plan to down the track. Each machine receives requests that are handled by Caddy. Here&apos;s an example:</p><!--kg-card-begin: markdown--><pre><code>http://blog.lchapman.dev {
        reverse_proxy localhost:2368 {
                trusted_proxies private_ranges
        }
}
</code></pre>
<!--kg-card-end: markdown--><p>Note how HTTP is specified: HTTPS is terminated at the gateway and all of my self-hosted services don&apos;t need to worry about TLS or certificates. I do need to turn off TLS for some softwares, e.g. GitLab, but that&apos;s usually provided as a config option.</p><p>The unencrypted request is received and passed on to Ghost on port 2368. Without specifying &quot;trusted_proxies private_ranges&quot;, Ghost will try to redirect requests to https and there will be a request loop, but that&apos;s a story for another post.</p><p>Check out <a href="https://blog.lchapman.dev/traffic-management-with-caddy/">Traffic Management with Caddy</a> for a full writeup about how I use Caddy!</p>]]></content:encoded></item><item><title><![CDATA[Hello World]]></title><description><![CDATA[<p>Welcome to the blog! I originally intended to build a website but have since decided that a blog would be perfectly adequate.</p><p>I will use this blog to share my learnings. I often find myself learning from other blogs, so it&apos;s time to give back.</p>]]></description><link>https://blog.lchapman.dev/hello-world/</link><guid isPermaLink="false">643e2ff11b701c0001604019</guid><dc:creator><![CDATA[Lachlan Chapman]]></dc:creator><pubDate>Tue, 18 Apr 2023 05:51:56 GMT</pubDate><content:encoded><![CDATA[<p>Welcome to the blog! I originally intended to build a website but have since decided that a blog would be perfectly adequate.</p><p>I will use this blog to share my learnings. I often find myself learning from other blogs, so it&apos;s time to give back.</p>]]></content:encoded></item></channel></rss>