<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Kashyap Kopparam</title>
        <link>https://kashlabs.com</link>
        <description>Your blog description</description>
        <lastBuildDate>Wed, 04 Nov 2026 07:34:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Kashyap Kopparam</title>
            <url>https://kashlabs.com/favicon.ico</url>
            <link>https://kashlabs.com</link>
        </image>
        <copyright>All rights reserved 2026</copyright>
        <item>
            <title><![CDATA[Homeprod: when the homelab becomes production]]></title>
            <link>https://kashlabs.com/articles/homeprod/</link>
            <guid>https://kashlabs.com/articles/homeprod/</guid>
            <pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>A homelab is where you try things. You spin up a Kubernetes cluster on Raspberry
Pis, put Ceph on a thumb drive, and learn a lot. Nobody minds when it breaks,
because nobody was using it.</p>
<p>Mine stopped being that. Photos, documents, the family tree, the budget, the
camera feeds, the music, this website — all of it lives at home now, and other
people in the house notice when it is down. That changes the rules. Uptime
matters, restores have to work, and "I clicked something in a <abbr>UI</abbr> six months ago"
is no longer an acceptable description of how a service is configured.</p>
<p>So I started calling it homeprod, and I rebuilt it around one requirement: if a
machine dies, I should be able to bring it back from a git repo and a backup,
without remembering anything.</p>
<h2 id="the-shape-of-it">The shape of it</h2>
<figure class="kl-figure kl-wide" id="fig-01" aria-labelledby="fig-01-caption"><div class="kl-figure__frame"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-d2-version="v0.9.0" preserveAspectRatio="xMinYMin meet" viewBox="0 0 987 1317" class="kl-diagram" role="img" aria-labelledby="d2-1593645617-title d2-1593645617-desc"><title id="d2-1593645617-title">How homeprod is put together</title><desc id="d2-1593645617-desc">Internet traffic on ports 80 and 443 reaches Traefik on docker-pi, a Raspberry Pi 5, which serves mywebsite and homepage. Admin access comes over the tailnet to a two-node Proxmox cluster running media, photo, document and Git services. Komodo Periphery on docker-pi reports to Komodo Core on the cluster, and the cluster backs up to a separate Proxmox Backup Server.</desc><style>
.kl-diagram .host &gt; .shape &gt; rect { stroke-width: var(--stroke-line, 2px) !important }
.kl-diagram .node &gt; .shape &gt; rect, .kl-diagram .group &gt; .shape &gt; rect, .kl-diagram .zone &gt; .shape &gt; rect { stroke-width: var(--stroke-hair, 1px) !important }
.kl-diagram path.connection { stroke-width: var(--stroke-symbol, 1.5px) !important }
</style><svg class="d2-1593645617 d2-svg" width="987" height="1317" viewBox="-1 -1 987 1317"><rect x="-1" y="-1" width="987" height="1317" rx="0" fill="none" class=" fill-N7" stroke-width="0"></rect><style type="text/css">
.d2-1593645617 .text {
	font-family: "d2-1593645617-font-regular";
}
@font-face {
	font-family: d2-1593645617-font-regular;
	src: url("data:application/font-woff;base64,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");
}
.d2-1593645617 .text-bold {
	font-family: "d2-1593645617-font-bold";
}
@font-face {
	font-family: d2-1593645617-font-bold;
	src: url("data:application/font-woff;base64,d09GRgABAAAAACA0AA4AAAAANkAAAWZmAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAGAAAABgfymFEWNtYXAAAAGkAAAA0wAAASwH1wgYY3Z0IAAAAngAAABsAAAA5jI0af5mcGdtAAAC5AAABxcAAA4MYi8FgGdhc3AAAAn8AAAACAAAAAgAAAAQZ2x5ZgAACgQAABBwAAAYwD9ltMpoZWFkAAAadAAAADYAAAA2O1Toe2hoZWEAABqsAAAAJAAAACQYw+k+aG10eAAAGtAAAACeAAAA8EeoJ1Zsb2NhAAAbcAAAAHoAAAB6wo68gm1heHAAABvsAAAAIAAAACAFMxcdbmFtZQAAHAwAAAM6AAAJHd19ynhwb3N0AAAfSAAAACAAAAAg/70ASXByZXAAAB9oAAAAywAAAOGg8z2vAAQFdgJYAAYACAUUBLAAAACWBRQEsAAAArwAZAIDAAACAAAJAAAAAAAAoQAC/xIA//sCAAAUAAAAAFVLV04AwAAg//8HYv5wAMgHYgH0YAABnwAAAAAEHwW0AAAAIAAFeJyMzslKWwEcxeHvNmmaNGmbdHa+TjExifMQdSWIIIqIIIg730BcuvGhFATF6VF8D/Ev3oVrOdsPzg+JnAQV+SRFXSqvINXU0tYxbVHXhi079hw4dOTEmasIpBrvbkbXuk3bdu1n7tjpm4snNVUlZcV4ied4jIe4j7u4jZu4jsu4iPOs4iNLrFi1llUtm8p+Z82Zt+CTnLzPCr4oKvmqrOKb736oqvnpl9/++Ouf/3r06tNvwKAhqWEjRo0ZVzehoWlSS1vHEq8AAAD//wMAv2Es2gB4nGIgHXxi+MRwh+EO6xYGBhZ5BoZ/BQgWshzrL4QsjM3Qz9DPkMeQx8TBKPzvxT91JjNGhX93/kkRqw+kC6aXUebffSZNsH51mDxbOYsPew6IZO5ma2BxZu9la2FxYK4DAAAA//8DAERNOKJ4nKyW+XfbxhHHd0GQOiJLsnXYDVJ3kDVUl1jQSus4jM04ClYU46hpaVluAadpAZFy7yPp5d73xfwz36XaV/e3/Gl9syBVyZHS1/eqHzRf7Hx2Z3dmsASEJoiHWTcn2n0qFu/vovHgUYabAa7nxWMaPczgReW/ZsWsGAzUQRCGEDmEUdtjIYUp0gRSg4rHCTytQhUmqGkaHtVW10RqsGKoKFLrrZrURjUDz+w/ISwoeMaUQ/j9J2PP80yRIjx8IeTR8eKaTF8geEal4xW5YopUQfSzw3y8Lj0X0NeoxVgzGcfDujETIKAh4cM+/I1H4+vygukOumh0sxC1KN97JwtVGIwyQr+fhdjKA0KbVTvPyVZ0OcT1fhZOngib7N9k8sN+Ro9pNCoJ8/2sCAjEvnlWt1jdKoIiz/MAXoQFM4DYyyB2GQ6xYIJdXGV1dbd8uiwGTDyti4M8H5Y5ZJznkxPkNMS6UWmeoK6pS/CjckiYMf0MMyrFrEqDMMwhiwQNl27UYhramYOU2MnHDart8380iu4A9WZImDU0ohFkbDfrEfyN+1nRD8q9PFN5mBO2HmSQccB5mWwlwYzGnInHwqvKPKsxp1JFECot4R08hhxAFphpJpjTxLtdNIOnvjggXgFbRc5Ise12O6/Hc4vCdNNmeNw4z+nTjbRQrSJjBWHgRwV1R6rkorpki4ALAgqwdZww1CJVblchLpwzHdf6GUSArbMmLXL/q/TowoKodftZGKgwb4YJlrT1vC6G5XaCZQ1ZEGHJvMUnIyypNMcyP+1lhGVXr4uasOySQk99MRipEhdNQaOCcFGlKsElvbufWX+4nV/DhUP1JMGK3r2f7T6oBoMwv4YVN76qrbhkHmb20iUDWaa4GPMrBy9K7RL/W/aiFHJdEWpRP7OcTvhROhoRh11uhgqynOqg8vMUL3LeHEumh2XTK+CdLtY5JbRCrKhtSANxdyyldNVb08IKr7uf4ZJKqYtFleKCQqNIqfjnlStSXBQrIk1TzsCqSiFLuzob44M4eDFPsK6tWIsTXNZWsr2ircf2E9rW2D6vrc820LbO9gVtG2w/qe0M26vazrL9lLZzbGOtpvlHo9jdzxS1IN/ltyWBPuFcP3a+VzmTE86NY+f7lZO0wFJ87jkhy39UR+VznjxfqK2gOMGL2kq2SluP7TVta2wjbX22G9rW2X5a2wbb69rOsP2MtrNsm9rOsW1p6riGvaGpwJWCjIIsDF/O/BK2uGc3NW7EuNFM8JIm6tE51VRlW/HF/rFEwKf/7LTEdrHR5Y7DS01bl2vdbDN3VfzcifScx9zU9LLb+ctaTJjuR2NCxmfuhcfF+t8F/23fVW17U67xWW9p6lDvnP1DmLKd4BXdutxJ0P5vKKQZtBO8qq0n1iNqUY+vBHjRvdGop3qqpOwg4FtXpeO2lGurzQS3NcQ6LqsUfgQ/cphdECmeM/HhqKWIOqN2gjunMWo5kNBQ6ZQmFHynbN3PjnyqU3Dkb9Sfz1O+aecNjZSboXYKNMyzr2vBt131q+SbYqhQN+Wwn8E3ZYC6Kfime3ZOqYjgb6idsh0ozJsd/sWaNy5KQWcFURxFoWEKLkY9KlH/yKrwN0r+teJN1KJiOLlJ/xMrT9CZ5oKIUN+Y5EJ12gleO3Zh3vl3VI+DchXvTn3uMFWmIfazFnVU6H5vvWiS1fpxKdCIUI/unfx2qYp4RguoSbUUt/zrJ3ZipuUq+APn2SNPS7ylFbU4izu4bLJ+sJdn1MlbdlOuxgneOOXdC/qnvOmZcz9uhtG4HZ81aQpsa9yJR0Qd7rFR+3wUDdPCZpyg647MbbxRZb7Egkqro3ODKupQS7Un6+9oO+9H6XTK/9jSvf9XF/OZ+B7rqHYQnuiXMJ/ss6etuB1Ps/KmtuJOHKpJXlT7dAruaYi16rUfC37DV1q41Uzw1jnju9oKubqCV5oJPq/xajPB25zFrqIW7YxUOc3WFzQ3NN6OE3xRj4XYiRP09VhIFvf1WLqRPT2WbuQBM704wT4zLB4yw+JLzLD4sj4SQpg4QaaP+NMpTpDrI1mNPdJHshp7hznJ6ivMOfUuc059lTmnvsYxu3GCgmOyKDkmiwOOyWLAzJtxgiEzLA6ZYfGYGRZf53hiO07wDY7n1Dc5nlPf4nhOfZs5yeo7zDn1Xeac+h5zTn1fW9E5LuAP3BO24gTvVfKNOMH7nHT3lMYJfqitnDA/qiQzP3aMnDA/0Va8drzqT92Tm/GkkjzjZ5Vk/Ofaygnwi0oy8MtKMvArbcXd4/V+7Z4c/ptKMv7bSjL+O23lBPh9JRn4QyUZ+KO24vXj9f7knhz+50oy/pdKMv5XbeUE+FslGRhVkoEP9Pg592WLRjD2vVo3U2EQ5nkaY/YQtWv9J7Yu17rZZp78GwAA//8DAN4wAVMAAAEAAf//AA94nIxYe3SU1bXfe59z5pXMZM58MxlI0PgxHYJGxWscYr1tHbFKCEOwtataWg3XqtCCV3yUUsVWrdJCQRAUsGB4FFvLQ4QOBR+VQItIkQoijyC+Sq1I4dYqoCTz7dx1zkx4WPHeP3LWrJX17X323r/9++19QMAwANVL5UFABaShMXtF+gvVPaTyYQ58UkmfuhcQpA/l90ABCUXf86MAEN/yoxDQAiBgqJS9z6rq6WhZISNpHQ34etbVu9qFPpmL+tdfWJmI+1JQ+uH3p2q/QpmLUr19/tphcn3nV6ju28O+evmwbw/g7//52fxfZvyQNvq+M+o/F193Y+g5le/44KrsgObmAZcNFW3LHpl+7z09f7A+VnvFFx/4xnU9AYBgMYDcrPIQhAj0y54bLi8ToCRgTqEEQkkjAWDgEIGIjdgcCoUioYjWMe0LVdU56KJ2tatd1Ih6Md6Gl+JtXPDe44I4gw/zR2IK1XrthTtwOM+nM7ESl/DVfBBAwJMA8rDKQwAiEIem7EAdFSSVlEiqLORTUpDI+bGYsxEgpckVDBziQyGwBextKiqCwYp4RdyJBSPBSLwyGgj1rHNc1P6kRjfdkKl1NdrzSQHYY2MnP1c4Rj/Z9GeejDesXMmtKs9H+KP5G/ktPrh+yZhlfDFN+vX1C3kYAADCWAD5ispDGfTNpv0+BEmIOUAs3QWIRAsI0SiataNj0qREo5t0MYGu3x2Ls2iB6/2J38Vq+pLrtZCDD1P0ED/4Nt93CAiWAiit8qCgDM7P1gX8gqQgzAFRyYM8KVqfz1fmK4s7UVWKEt00ZtDVS2kbPsKjvX40GHvwfn5O5b2PeYz37iGc+DqOLfQxsbQByB22zqnsWUAgJIkRJ4UiZaNs1lrrqApV1zlu0tVuwtVum+ioLvyLvukt5g6Vb+dzX/FeMvZGAchOlQcNl2QbfEgS/ShkRQgECZEDCSaWkx0QqRZQqlE1x3TM0TFfqNdJ2WrIuBlX+91R+AgtqPY2835MYO0P+Rly3/fepzhOJX2IJ+zjn/6oY6O3vcPWpxu7ZeBmzwQBgAJGSCQaOEQV8WoC0lHjCi1Oi39inXeYj2AZlWOIj9I93k9V3ltALR25Yt2XA8gXbd1T2bMM8qkFjFGJAN1BnLDc4Gq3webL7y7HB/nuuPhnnO/EyfGCbpfU3t7J7UW7vwWQe1UeovAf2fPLgyQojCAiQAgi95n3j8eMG+MljSlzdV1vOg5Ru2Itz/6VieIPeAMG+TacyvuO4Thvqsp7M3EYRnk138fX8eqi7zcB5F8s1npmK4sVQaThQNRIzVpHhQWVdv3um6joH9WFOpXvTLd/Ks9nZqsDRASYQzQ2BNrvHe1ELfrrsV7X6xSmdGrxavp++eHV3qzyIypfyIq2jpxYU2g0nHMzgIqqPPihAi7M9ouEQ0ElEQKnx34gEKgImEa32NeGd9BtwAymMIE34zIcik/x1fz0buzNbw3nN7B3ETLeP/k27+BhHL8bx/HPd/MDlvPmA6iY9R82/kNBQVKKIuUZJI+AIjWbJNn4AoFAOBCOayfqM1Fa79otUst8Ecfr+QnvIz5K87a+zCNUnrfys9zGRG1bZiz2jlqfKwGkp/JQDjH4cvaSishxtgsoQhI5HyogPIXrSqQbDodj4VhcxxJRv+1N1Aq1mxHH77CS3sLR3I61fNTjjXyE7tyxg2epPO/hdz7kB7jrEF7jPU8fto2fy32LeFgGIJ9XeaiAc7Nnh8vLQoZxA1CsgE37wCEA0M1vCZ2wFW5wGzKGdJKuP2FYbhk9cIvXEwO0rdqbfQv9nT/x4u9PuBI/2YUXvD/hSg7s4q3W3z0A8hOb8+psD0KAbh9F+tTamsdSJ9HFfEVMXhPjK7lDvt7e3pk+FYdfyLpBnwCJgDljzKJZDC/eNRaLaWOsQbs66ZrT7y7GUAte1YLBl71xXXJV54Lx42VL5+Aue7dfAchXVR4cw/M6WlFeFvL7FCGUAxr7iAbpAI3QHI/HHWM7jahTRvTcBKKut7/EJm7nI4fxRp6LITwbgx9xK97AH3dhltd1AV6BC/F+HOzxH/BSRH6er+Mf8ypAmAkgC5aba7K94ERqTpKXmC6GZBNkxGUmHfKcfmJTfy8hUv06eZWse21p55YdBmt3dB2QO2QNhMGB/tn6Mh8BOrEKQUpIyiGKQQAgBUjDz5bbmkp1cGI6FjcCkFbpTNrN1Bp3OpVOZ1x/QvzDe5HX4xfo0pHez6ror3/7g3cEN3F63h0T3zny9NRv/nl/YdOT123+es7cYYbV9w7wQxj6ZFPlZabHBSGEsMlk1EK8EUqtVSlCyTonrdOms9INulbpGajwAN7OY72z6eZPXuBr1eZ+3u8m8mv8ZX7mlZex7ieDvO1FLN8FIN+SNafR6abT67Q/kXT97l04nv7Yx7vngw/op328L+P3sBlH7t2+bvteILi/6z25T3VAsNgpFZEyQQoJc34fQRMiDupmrCaDwUZsdpy4IdO0MrHotMmedv1aFLxP+CGchK+iWs4V+CQ//PFdM/Bc/CMOxKf6dfh2N9F5W3gnkMXDu7IGysGBy7JfqYhIlEoqRBUAIixyBZLC41xxSozhcNixfOHELF8YkdV+18ZqOcufmIm34oGjT3lf66S+1d6PcO7ocbzvMN6I1+CNr/DRcf23vkS3DPqvIQfCJr9TurpUL9UBFWZKCUpCgTmJqBB8CIYsLYYGDQGfr9HXrHVMx3TUHzqjDt2MW0q0HSbwA34cv4t+Kqvill9gJ6suoLe3YA/M7F2wdYtXY+pJ8BCPkdtsPTWclz1HR8NBnwFPEATmoKQKTYaerOMcNTuxSssfym3IKBRovGJaG5I6i86aylm+F3EiPctHvTvwTLyPn+7ML9j5fazE0fjVzgm8mf44ALf+JW/8I8wBkHtUB5RBVTYZDBS5IGCqbTmg0qFQjzqsVdrVBrLCFbse4xZmJEL8EjLT7ruvLHxUAuyhwhtA8AyA+K0vBz4IQTrb2zAX5kLBgMCm7o5vKqlNrKjGttW1KxqSfprFvaspM435lra1a0XmwBOFCerR1autthPkeYx415eDEETggux5fkQKGJGmXCRcLqjppC4vMXpONDs6ZlNmGdJ4Qhcbkv48zpjM5dU0eBofY8RZbWvX/mahuOTAE4U63uxPl3wirAJQQdVR3BrKEKjcjDGU8ykSTUAASBYaTUMkFttCO1pbarGTTFrXo4uYcMU2fnwVF1C+gOOwihvwN3xgIfbl/mpzv8IcnMR9ecprJk6Eq7u65JuqA0JwTrZWIaDP9B7luhM46FOdbikT7XSmLfj38ZhqfHfaDBxZxf3wIvHG+08UMgsOGNvTAeQ+2R9ixnY0Ul4setAIYkWYqMmSpEAqIkAXVQBr/Tql7VGfTCfTSZ2S4hsT915779uLzkWaj2esf1tEL7+k8J6IXt6/sE/W3PLA9OkrCncX45lrcCb7QxmksmcF/YSgDCoCiKZkTUOopDnasXiwgKtPpo3U7LaAa7uAYvilbrzJmrFTLNpMDxm9PSZrPnfGavp/zFgNmdqUP5G8Bx/Cm3Ai387zmXkuM47EoTiSZ/MKnr1353M7dz6302JxBo+Rh2V/y5eZ7IWRcHmZkuQXgDmB0HTC63Dj1RQsR2atrAhV6ERxtbEhoqGLohA8dTEFjRDw+dxZlAJZc/f0ohDwGGo5RQsWWO4s5bQsZLPqPyWrRd3RJzVxfTKtkkr8fToPZV53MWn8InYh/fW/BxWOyZopi7wNaWt7DYD8naz5nJmp6fQzU0NpZvK7a2hBk9d3jUhUe7cNpp1rCntfuPx8/O7ifS9cfg63Lt9nfY3p6pJL1LlQYRiwDMis5AaSRjwFmsnHkP/xbSRm4G72BDMYujqtDd8m3YSL9/P/YAyBzvZ2/QJbvTfxIly6Bb92Pt+3hRcv32t8ze3aL/fImpNxaAarAOIgwhPC1o3DZFq7OlVrZlBsfQwXILVd4B3iDUg48O4rRbjznbEPYVykbBytAHKXrIEQ1GR7BZQUoLrXh+N8F9cG305psdHotqLAXlgNXTyCO2g4ruU7uQFfwil8mTe/2DtGI9+WNRAzmhQNh4LWdlBY4QcBKO4t3t2AzQ6F8bilH0fXa8fJ1Ccc80ukhEjNfP2u++c+MO71PesmTVpHOdzO3+bROGP6w+bEX/N5Xh4n853W7woAuUHWfM6s2PTvs6JIpDKIGTeBxrFlvfW8rWM/DuZVKPCCY/t5FQ5mbx76mRftwq9jza55fAxp0S5+it/ZBQizeIx81WKvbzZdCpcCJ3wa3Jn5NyeaY/HESWJohmBTMHQxSWKN5/FLKB7FXyHwVu/xZQu8SejQE94vbx1IDxklBIRfWl6qgaBRKmWV6hSNOv1UupBWeZdXi99nvW9Se1XBzdOs15Z4wy1/i+N7n4AycGBA9tKAIpAxHQlLhKAkhcJyuQI72ZS2bh8q1e2xvLzcKXfijmPmi+o6rE+k1Ke2QfrWhg0c/qyVUFzb0XSapbC4W/xd5SFkekAKEjZqM4YbGHWvFlrHSy2tVSZdb7psMS7m2fj4Npzc+ad2se/G935QXJsBYRqAPGh3vrpsXz8CBgjFyYL16SckKyoNaMduE1fCnSYqf+std2kmXuFdKGKdb3xMs1+9+dDLPMBb3m7w+BiA3K/yEC9ulcGAJBFFpJgWhjNEDggQCI0WyxaQ0q513bxRqc3qbnjDLHa1bsaeDa5K+FUC3cfo9hef8f5JsnWJN6eK/oaRjRji5V6v9Svve+TFJ2+dvH4/b9x3jHeY+iIMBFDnqC0QgVyekLBp8NPpq67NJiyhCAn2wcleolE2V2erzDMUXQOf8c9vZYPm4UM75moOJtHV9bo+kdIp7Q6k72zwPqaI19bmeUSXicmTC8fwa/z8dp6Md5p7zDcaYHfbC7P9zAQkbTGleYnF4/PqKYlwtD4lEY6bdIWtwUq66p1fe99Ni03xghejBnR5ijfs0O9//jNB7bzwCP+e14OAu0o7XBgScAZcnb2qV3WPZMAvRUDJIBGpuBOtKAtZBf4/HzYrKyORyjMqz6jqGUlEEvHKhH3Y1A21Gk953fQnkqWBPnnXkS3YGw9hassRXsUOTlqyhPcsXYp9Wlt5T2srhrB2XR4Jy/kwc34dt/PRbRMXPfjgook0fta4qVPHzbI1xGN8g/gB3go+6JmtNH2Ag7rRat6jolY1XY21Top8eCt3jF27Em/lh/mGL+JNgCTwX+J20y7QI5swBuGa0veAQ3X35y4J85EYzg8Dwryui+V0yws9s5WECDjBpASaLWsIbZbDJLo4j9qxyqt9yTf+mOlZvJ7qZA8hP99XwsXr6VnvSiFNv+A4HiPn+waABicb7V4Rc9Dcw84DEIslU32IMhfFYg31PqIE3Tn14IrCnA/nzftwTmHFwal4G8/hw9NGo8AfFzrxx6hGTePDPGc/IOzkMYKP2y5lLYfdtolq6ytjsUScyJ/qH4tlBJ1qW1yGN2H5tNHs8f2FTr6fO0dNw3K8ydheATPkBvk4lJlt2gL5mn9rK5RDTzxLOqZlzBSQ0u4KfPQAPsqjDvAoIfExHtHOI3E2wP8CAAD//wMAsGm2/QABAAAAAWZmiuMdqF8PPPUABwfQAAAAAOXRGlsAAAAA5dEdt/A8/FogWwjdAAAABwACAAAAAAAAAAEAAAdi/nAAyAV28DzlGyBbAAEAAAAAAAAAAAAAAAAAAAA8eJwszr8qxXEch/HnR8+1uQCbwWixKPWkUxY7iSJ/8icLIh1RbK7C6D5sp1Pf8fOqz7vH2DAwno1HI+PF+DG2h78ZT8bfuLeMO+PdeDX2hz8YZ8aucWrMjINhh8aRcWV8Gx/Gp7FunBjXY2f5d298GTvDb8bG3Dg3LkfDsvnYuDDWRtPMpn9bWTVubdq0ac/4NeYLAAAA//8DAJw/PzAAAAAAAGAAYACcAO4BHgFUAXwBvgHqAhYCVAJyApoC2gMUA1wDlAO2A+IEIgRKBIwEwgTwBSoFcgWqBeoGFAZCBnwGtgbkBx4HTAeIB8gH9ggsCGIIkAi8CPgJNAlqCZYJ5AoOCkAKhgrKCwQLaAuIC6ILvgvYDAYMNAxgAAAAAQAAADwCkwA4AAAAAAACAVACEACNAAAC4RJ4AAAAAHictJS9bttWFMd/lC0rhpOiH0AzdDnxENmuQ0p2pmSyBTtWrSiFpMYBOlHirUSAXyDpJHKHDEWHjh26d8rQd+g7dChQdOoT9B0K3lzFsizXFtBqEH78n697zuEl8Jn1BRbFz2JL/xdcosKW4WW2qBsus8HXhleo8p3hCr/xi+FbrFlnhle5Z31veI271lvDt7lr/Wr4Dl3rd5awllex6JWqhkt8UPrW8BJe6QfDy/RKfxsu8+XSnuEV2ks/Gq5YPy/9ZfgWn5bfGF5lv/yT4TXs8h+Gb2OvlA3f4e3KruEPqa38afgj1iurhj+mV/nc8Cf0Km+expGbJe5AyXE6TvI4ksM0PlORdFXon/ie0rAfB15HDU8DN715wHOVZn4cSd1+WKvJxlWBm+8NRn8nP5jkm6RrxMk49YejXHZqO7vyxM+PTvvb0owGtmyM8jzJHjnO0M9Hp317EIcGI/U6d8JJhc1teeXnI+moTKUvlSeHcZRL2w1VJtX356huTz3IXjqMo4tSW80qL1Q0K3VcT0tu5En1UvNVu6XyXKWhm/uDjh+osTRSN5L7ko+UTNmkp9xw0l5wrhc9XiF3my15lqjoXXctf6CiTEndrssNx+T0g7jvhK4fOa1m46DdPeApMREuGQkuAxTCMSljEnJtEw5JiTlD6acuihCfE3w81JSyT0yARwfFkFMCXFLq2DykRo3HfMUxJ7R5PKfuxarTNR9cqjlb8f/o4jmKlAzfxJ/3IWwsXHFzTsRF/8U6bhCTMCbFZ8iIHGGHGjvsIjzBJ+eIU/psIzSJGGDrkxe+OQkZj3BwGGrfkfa1GRATzqgRitfkOISXetjU+V8Zb9G7z/TkXqLw9BSKmOJ8bVxCbReqc+ZRZfsKi7BHylDr/+bVRl3r80Lv4zqvDi7eVC6XCO9K74ubrGLTQuk5F5MIccnxGdDBJ0AxRmiQ6qzCfURPr6g7P07oofT0ZrcXzPWf7HEx7y5NWgjPSMwtOd9dS5+/UIvtir7VdeQ/fpsc+gTE9LXVxSfCoUWTBge06bL4F+v6m75oRrlBzkTfzGJ237DBOkcoAt1b8c4tWvEe62z+AwAA//8DADSU34EAAAADAAAAAAAA/7oASAAAAAEAAAAAAAAAAAAAAAAAAAAAeJxi8N7BcCIoYiMjY1/kBsadHAwcDMkFGxnYnbYXJAQ4mKiwMWiBeA58qRyhHK4clmyaHNLsbFxQwVy2WDZfNnsWQzYlVrAgr9N+yQbRBsEGvgauBnYGBjYGbq2NDMJO+xkckCBIdCcDMwODy0YVxo7AiA0OHREgforLRg0QfwcHA0SAwSVSeqM6SGgXRwMDI4tDR3IITCIyMjLSgS+dLZzNnc2aRZtNlpWNT2sH4//WDSy9G5kYXDazprAxuLgAAAAA//8DAKAVNFgA");
}</style><style type="text/css">.shape {
  shape-rendering: geometricPrecision;
  stroke-linejoin: round;
}
.connection {
  stroke-linecap: round;
  stroke-linejoin: round;
}
.blend {
  mix-blend-mode: multiply;
  opacity: 0.5;
}

		.d2-1593645617 .fill-N7{fill:var(--ground, #f3f6fb);}
		.d2-1593645617 .stroke-B2{stroke:var(--accent, #086a9e);}.appendix text.text{fill:var(--ink, #0c2350)}.md{--color-fg-default:var(--ink, #0c2350);--color-fg-muted:var(--ink-muted, #455679);--color-fg-subtle:var(--ink-muted, #455679);--color-canvas-default:var(--ground, #f3f6fb);--color-canvas-subtle:var(--surface-sunken, #e6ecf6);--color-border-default:var(--ink, #0c2350);--color-border-muted:var(--accent, #086a9e);--color-neutral-muted:var(--surface-sunken, #e6ecf6);--color-accent-fg:var(--accent, #086a9e);--color-accent-emphasis:var(--accent, #086a9e);--color-attention-subtle:var(--ink-muted, #455679);--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}</style><g class="aW50ZXJuZXQ= zone"><g class="shape"><rect x="353" y="12" width="116" height="60" style="stroke:var(--ink-muted, #455679);fill:var(--ground, #f3f6fb);stroke-width:1;stroke-dasharray:3.000000,3.000000;"></rect></g><text x="411" y="47.5" class="text-bold" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Internet</text></g><g class="dGFpbG5ldA== zone"><g class="shape"><rect x="865" y="142" width="108" height="60" style="stroke:var(--ink-muted, #455679);fill:var(--ground, #f3f6fb);stroke-width:1;stroke-dasharray:3.000000,3.000000;"></rect></g><text x="919" y="177.5" class="text-bold" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Tailnet</text></g><g class="ZG9ja2VyLXBp host"><g class="shape"><rect x="16" y="277" width="831" height="300" style="stroke:var(--ink, #0c2350);fill:var(--ground, #f3f6fb);stroke-width:2;"></rect></g><rect class="kl-legend kl-legend-host" x="28" y="268.0" width="230.1" height="18" style="fill:var(--tape, #a8e4ff)"></rect><text x="143.0" y="281.3" class="text-bold" style="fill:var(--on-tape, #0c2350);text-anchor:middle;font-size:12px">docker-pi · Raspberry Pi 5</text></g><g class="cHJveG1veA== host"><g class="shape"><rect x="12" y="667" width="922" height="228" style="stroke:var(--ink, #0c2350);fill:var(--ground, #f3f6fb);stroke-width:2;"></rect></g><rect class="kl-legend kl-legend-host" x="24" y="658.0" width="137.8" height="18" style="fill:var(--tape, #a8e4ff)"></rect><text x="92.9" y="671.3" class="text-bold" style="fill:var(--on-tape, #0c2350);text-anchor:middle;font-size:12px">Proxmox cluster</text></g><g class="cGJz"><g class="shape"><rect x="266" y="1049" width="324" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="428" y="1084.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Proxmox Backup Server (own box)</text></g><g class="bGFu group"><g class="shape"><rect x="171" y="1143" width="642" height="160" style="stroke:var(--ink-muted, #455679);fill:var(--ground, #f3f6fb);stroke-width:1;stroke-dasharray:3.000000,3.000000;"></rect></g><rect class="kl-legend kl-legend-group" x="183" y="1134.0" width="137.8" height="18" style="fill:var(--ground, #f3f6fb)"></rect><text x="251.9" y="1147.3" class="text-bold" style="fill:var(--ink-muted, #455679);text-anchor:middle;font-size:12px">Also on the LAN</text></g><g class="ZG9ja2VyLXBpLnRyYWVmaWs="><g class="shape"><rect x="357" y="327" width="109" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="411.5" y="362.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Traefik</text></g><g class="ZG9ja2VyLXBpLm15d2Vic2l0ZQ=="><g class="shape"><rect x="419" y="467" width="127" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="482.5" y="502.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">mywebsite</text></g><g class="ZG9ja2VyLXBpLmhvbWVwYWdl"><g class="shape"><rect x="282" y="467" width="117" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="340.5" y="502.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">homepage</text></g><g class="ZG9ja2VyLXBpLmRkY2xpZW50"><g class="shape"><rect x="66" y="327" width="271" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="201.5" y="362.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">ddclient (CF + Namecheap)</text></g><g class="ZG9ja2VyLXBpLmF0dmxvYWRseQ=="><g class="shape"><rect x="670" y="327" width="127" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="733.5" y="362.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">atvloadly</text></g><g class="ZG9ja2VyLXBpLnBlcmlwaGVyeQ=="><g class="shape"><rect x="566" y="467" width="190" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="661" y="502.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Komodo Periphery</text></g><g class="ZG9ja2VyLXBpLmFsbG95"><g class="shape"><rect x="486" y="327" width="164" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="568" y="362.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Grafana Alloy</text></g><g class="cHJveG1veC5wdmU= node"><g class="shape"><rect x="62" y="717" width="340" height="128" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><rect class="kl-legend kl-legend-node" x="74" y="708.0" width="221.7" height="18" style="fill:var(--ground, #f3f6fb)"></rect><text x="184.8" y="721.3" class="text-bold" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:12px">pve · mini PC, ZFS raidz1</text></g><g class="cHJveG1veC5wdmUy node"><g class="shape"><rect x="422" y="723" width="245" height="115" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><rect class="kl-legend kl-legend-node" x="434" y="714.0" width="45.6" height="18" style="fill:var(--ground, #f3f6fb)"></rect><text x="456.8" y="727.3" class="text-bold" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:12px">pve2</text></g><g class="cHJveG1veC5jb3Jl"><g class="shape"><rect x="687" y="717" width="197" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="785.5" y="752.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Komodo Core (LXC)</text></g><g class="bGFuLmhh"><g class="shape"><rect x="221" y="1193" width="261" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="351.5" y="1228.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Home Assistant OS (Pi 5)</text></g><g class="bGFuLnBpaG9sZQ=="><g class="shape"><rect x="502" y="1193" width="261" height="60" style="stroke:var(--ink, #0c2350);fill:var(--surface, #ffffff);stroke-width:1;"></rect></g><text x="632.5" y="1228.5" class="text" style="fill:var(--ink, #0c2350);text-anchor:middle;font-size:13px">Pi-hole (Pi 2B, LAN DNS)</text></g><g class="cHJveG1veC5wdmUuc2VydmljZXM= list"><g class="shape"></g><text x="232" y="780" class="text" style="fill:var(--ink-muted, #455679);text-anchor:middle;font-size:13px"><tspan x="232" dy="0">immich, jellyfin, frigate</tspan><tspan x="232" dy="14">paperless, step-ca, fileserver</tspan></text></g><g class="cHJveG1veC5wdmUyLnNlcnZpY2Vz list"><g class="shape"></g><text x="544.5" y="786" class="text" style="fill:var(--ink-muted, #455679);text-anchor:middle;font-size:13px">Forgejo, Navidrome</text></g><g class="ZG9ja2VyLXBpLih0cmFlZmlrIC0mZ3Q7IG15d2Vic2l0ZSlbMF0="><marker id="mk-d2-1593645617-957925861" markerWidth="10" markerHeight="12" refX="7" refY="6" viewBox="0 0 10 12" orient="auto" markerUnits="userSpaceOnUse"> <polygon points="0,0 10,6 0,12" class="connection" stroke-width="2" style="fill:var(--accent, #086a9e);"></polygon> </marker><path d="M 429.915985 388.5 L 429.915985 417 S 429.915985 427 439.915985 427 L 472.75 427 S 482.75 427 482.75 437 L 482.75 463.5" fill="none" class="connection" style="stroke:var(--accent, #086a9e);stroke-width:2;" marker-end="url(#mk-d2-1593645617-957925861)" mask="url(#d2-1593645617)"></path></g><g class="ZG9ja2VyLXBpLih0cmFlZmlrIC0mZ3Q7IGhvbWVwYWdlKVswXQ=="><path d="M 393.583008 388.5 L 393.583008 417 S 393.583008 427 383.583008 427 L 350.75 427 S 340.75 427 340.75 437 L 340.75 463.5" fill="none" class="connection" style="stroke:var(--accent, #086a9e);stroke-width:2;" marker-end="url(#mk-d2-1593645617-957925861)" mask="url(#d2-1593645617)"></path></g><g class="KGludGVybmV0IC0mZ3Q7IGRvY2tlci1waS50cmFlZmlrKVswXQ=="><path d="M 411.75 73.5 L 411.75 323.5" fill="none" class="connection" style="stroke:var(--accent, #086a9e);stroke-width:2;" marker-end="url(#mk-d2-1593645617-957925861)" mask="url(#d2-1593645617)"></path><text x="412" y="205" class="text" style="fill:var(--ink-muted, #455679);text-anchor:middle;font-size:12px">80 / 443</text></g><g class="KHRhaWxuZXQgLSZndDsgcHJveG1veClbMF0= private"><path d="M 919.75 203.5 L 919.75 663" fill="none" class="connection" style="stroke:var(--accent, #086a9e);stroke-width:2;stroke-dasharray:8.000000,7.892511;" marker-end="url(#mk-d2-1593645617-957925861)" mask="url(#d2-1593645617)"></path><text x="919.5" y="440" class="text" style="fill:var(--ink-muted, #455679);text-anchor:middle;font-size:12px">admin UIs, SSH</text></g><g class="KGRvY2tlci1waS5wZXJpcGhlcnkgLSZndDsgcHJveG1veC5jb3JlKVswXQ=="><path d="M 661.25 528.5 L 661.25 612 S 661.25 622 671.25 622 L 775.5 622 S 785.5 622 785.5 632 L 785.5 713.5" fill="none" class="connection" style="stroke:var(--accent, #086a9e);stroke-width:2;" marker-end="url(#mk-d2-1593645617-957925861)" mask="url(#d2-1593645617)"></path></g><g class="KHByb3htb3ggLSZndDsgcGJzKVswXQ=="><path d="M 428.75 897 L 428.75 1045.5" fill="none" class="connection" style="stroke:var(--accent, #086a9e);stroke-width:2;" marker-end="url(#mk-d2-1593645617-957925861)" mask="url(#d2-1593645617)"></path><text x="429" y="977" class="text" style="fill:var(--ink-muted, #455679);text-anchor:middle;font-size:12px">backups</text></g><mask id="d2-1593645617" maskUnits="userSpaceOnUse" x="-1" y="-1" width="987" height="1317">
<rect x="-1" y="-1" width="987" height="1317" fill="white"></rect>
<rect x="19" y="276" width="222" height="2" fill="black"></rect>
<rect x="15" y="666" width="130" height="2" fill="black"></rect>
<rect x="174" y="1142.5" width="129" height="1" fill="black"></rect>
<rect x="65" y="716.5" width="214" height="1" fill="black"></rect>
<rect x="425" y="722.5" width="38" height="1" fill="black"></rect>
<rect x="380" y="193" width="64" height="14" fill="black"></rect>
<rect x="865" y="428" width="109" height="14" fill="black"></rect>
<rect x="401" y="965" width="56" height="14" fill="black"></rect>
</mask></svg></svg>
</div><figcaption><p class="kl-figure__caption" id="fig-01-caption"><span class="kl-figure__number">Fig <!-- -->01</span>How homeprod is put together</p><div class="kl-key"><span class="kl-label">Key</span><span class="kl-key__item"><svg viewBox="0 0 34 18" aria-hidden="true"><rect x="1" y="4" width="32" height="13" style="fill:var(--ground);stroke:var(--ink);stroke-width:var(--stroke-line)"></rect><rect x="5" y="1" width="14" height="6" style="fill:var(--tape)"></rect></svg>Host: a machine or cluster</span><span class="kl-key__item"><svg viewBox="0 0 34 18" aria-hidden="true"><rect x="1" y="4" width="32" height="13" style="fill:var(--surface);stroke:var(--ink);stroke-width:var(--stroke-hair)"></rect><rect x="5" y="2" width="12" height="4" style="fill:var(--ground)"></rect></svg>Machine inside a host</span><span class="kl-key__item"><svg viewBox="0 0 34 18" aria-hidden="true"><rect x="1" y="3" width="32" height="13" style="fill:none;stroke:var(--ink-muted);stroke-width:var(--stroke-hair);stroke-dasharray:3 3"></rect></svg>Zone or loose group</span><span class="kl-key__item"><svg viewBox="0 0 34 18" aria-hidden="true"><path d="M1 9 H27" style="fill:none;stroke:var(--accent);stroke-width:var(--stroke-symbol)"></path><path d="M26 5 L33 9 L26 13 Z" style="fill:var(--accent)"></path></svg>Connection</span><span class="kl-key__item"><svg viewBox="0 0 34 18" aria-hidden="true"><path d="M1 9 H27" style="fill:none;stroke:var(--accent);stroke-width:var(--stroke-symbol);stroke-dasharray:4 3"></path><path d="M26 5 L33 9 L26 13 Z" style="fill:var(--accent)"></path></svg>Private-network connection</span></div></figcaption></figure>
<p>Two Proxmox nodes in one cluster, a Raspberry Pi 5 that does all the Docker
work, a Pi 5 for Home Assistant <abbr>OS</abbr>, an ancient Pi 2B running Pi-hole, and a
separate physical box for Proxmox Backup Server. The backup server is
deliberately not part of the cluster — a backup that shares a failure domain
with the thing it backs up is not a backup.</p>
<h2 id="stateful-things-get-an-lxc-stateless-things-get-a-container">Stateful things get an LXC, stateless things get a container</h2>
<p>The split I settled on is boring and has held up well.</p>
<p>Anything with a large data directory — Immich, Jellyfin, Frigate, Paperless, the
file server, Syncthing, the private <abbr>CA</abbr> — runs as its own <abbr>LXC</abbr> on the Proxmox node
with the <abbr>ZFS</abbr> pool. Those containers are pets. They get snapshots, they get
backed up by the cluster backup job, and I do not move them around.</p>
<p>Anything that is just a process in front of some config — the reverse proxy,
this website, the dashboard, the dynamic <abbr>DNS</abbr> clients — runs as a Docker stack on
a single Raspberry Pi 5. Those are cattle. Their entire definition is a compose
file in git, so the Pi itself holds nothing I would miss.</p>
<p>That second half is the part worth writing about, because it is where I made the
biggest change this year.</p>
<h2 id="git-is-the-control-plane">Git is the control plane</h2>
<p>Docker on that Pi used to be managed through Portainer. It worked, but a good
chunk of the setup existed only inside Portainer's database: stacks pasted into
a textarea, environment variables typed into a form, edits made at 11pm and
never written down. The compose files in my repo had quietly drifted into
fiction.</p>
<p>I replaced it with <a href="https://komo.do">Komodo</a>. The topology is a Core and
Peripheries: Core runs as a Docker <abbr>LXC</abbr> on the Proxmox node, each Docker host
runs a small Periphery agent that connects <em>outbound</em> to Core over the <abbr>LAN</abbr>. The
agent dials out, so the host needs no inbound ports and no credentials stored on
Core's side.</p>
<p>What makes it stick is that the stacks are declared in the repo, not in the <abbr>UI</abbr>:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">toml</span></figcaption><pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">stack</span><span class="token punctuation">]</span><span class="token punctuation">]</span>
<span class="token key property">name</span> <span class="token punctuation">=</span> <span class="token string">"traefik"</span>
<span class="token punctuation">[</span><span class="token table class-name">stack.config</span><span class="token punctuation">]</span>
<span class="token key property">server</span> <span class="token punctuation">=</span> <span class="token string">"docker-pi"</span>
<span class="token key property">project_name</span> <span class="token punctuation">=</span> <span class="token string">"traefik"</span>
<span class="token key property">git_provider</span> <span class="token punctuation">=</span> <span class="token string">"forgejo.internal"</span>
<span class="token key property">repo</span> <span class="token punctuation">=</span> <span class="token string">"k/homelab"</span>
<span class="token key property">branch</span> <span class="token punctuation">=</span> <span class="token string">"main"</span>
<span class="token key property">run_directory</span> <span class="token punctuation">=</span> <span class="token string">"stacks/traefik"</span>
<span class="token key property">file_paths</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"compose.yaml"</span><span class="token punctuation">]</span>
<span class="token key property">environment</span> <span class="token punctuation">=</span> <span class="token string">"""
ACME_EMAIL=[[ACME_EMAIL]]
CF_API_EMAIL=[[CF_API_EMAIL]]
CF_DNS_API_TOKEN=[[CF_DNS_API_TOKEN]]
"""</span>
</code></pre></figure>
<p>One <code>resources.toml</code> lists every stack, which host it belongs to, and where its
compose file lives. Komodo reads it as a resource sync, so adding a service is a
pull request, not a session with a <abbr>UI</abbr>. Secrets never enter the repo: <code>[[VAR]]</code>
references a variable held by Komodo, which it expands into the stack's <code>.env</code>
at deploy time.</p>
<p>The loop closes with a webhook. I push to my self-hosted Forgejo, Forgejo posts
to Komodo Core, Core tells the Periphery on the Pi to pull and redeploy that one
stack. Editing a compose file and pushing it is the deploy.</p>
<p>One snag worth knowing if you self-host Forgejo: its <code>ALLOWED_HOST_LIST</code> for
webhooks defaults to <code>external</code>, which silently blocks private addresses. Every
delivery to a <abbr>LAN</abbr>-hosted Komodo fails until you add the host.</p>
<h2 id="the-edge">The edge</h2>
<p>Traefik v3 terminates everything. The router forwards 80 and 443 to the Pi, port
80 redirects to 443, and services opt in with labels rather than config files.</p>
<p>The certificate setup is the part that took the longest to get right, because
three different kinds of name need three different challenges:</p>
<div class="kl-table kl-wide"><table><thead><tr><th>Resolver</th><th>Challenge</th><th>Used for</th></tr></thead><tbody><tr><td><code>namecheapresolver</code></td><td><abbr>HTTP</abbr>-01</td><td>the public domain this site runs on</td></tr><tr><td><code>kopparamdnsresolver</code></td><td><abbr>DNS</abbr>-01 via Cloudflare</td><td>internal names that never face the internet</td></tr><tr><td><code>step</code></td><td><abbr>ACME</abbr> against my own step-ca</td><td><abbr>LAN</abbr>-only names, on a private <abbr>CA</abbr></td></tr></tbody></table></div>
<p><abbr>DNS</abbr>-01 is what makes the middle row possible: I get a real Let's Encrypt
certificate for a hostname that resolves only inside the house, because the
challenge is answered in <abbr>DNS</abbr> and never needs an inbound connection. The third
row is a private <abbr>CA</abbr> running as its own <abbr>LXC</abbr>, issuing certificates over <abbr>ACME</abbr> just
like Let's Encrypt does — the same Traefik config, a different directory <abbr>URL</abbr>.</p>
<p>Two <code>ddclient</code> containers keep the A records pointed at my residential <abbr>IP</abbr>, one
per registrar, because my domains are split across Cloudflare and Namecheap.</p>
<h2 id="nothing-administrative-is-on-the-internet">Nothing administrative is on the internet</h2>
<p>The only things exposed publicly are the two websites Traefik serves. Every
admin interface — Komodo, Proxmox, Forgejo, the dashboards — is reachable only
over <a href="https://tailscale.com">Tailscale</a>.</p>
<p>The trick I like most here is <code>tailscale serve</code>. Rather than putting Komodo's <abbr>UI</abbr>
behind the reverse proxy and inventing an auth story for it, the <abbr>LXC</abbr> runs:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash">tailscale serve --bg <span class="token number">9120</span>
</code></pre></figure>
<p>That publishes the <abbr>UI</abbr> at an <abbr>HTTPS</abbr> <abbr>URL</abbr> with a valid tailnet certificate,
reachable only by my devices. No port forward, no proxy rule, no extra login.
Tailscale <abbr>SSH</abbr> covers shell access the same way, which means the Pi has password
authentication disabled entirely and I do not manage <code>authorized_keys</code> by hand —
with one <abbr>LAN</abbr> fallback key, because I have locked myself out of a machine by
trusting an overlay network before.</p>
<p>Pi-hole is <abbr>LAN</abbr> <abbr>DNS</abbr>, so the internal names resolve in the house and the ad
blocking is free.</p>
<h2 id="rebuilding-a-host-is-a-playbook">Rebuilding a host is a playbook</h2>
<p>Earlier this month I reinstalled the Docker Pi from scratch — new <abbr>OS</abbr> release,
new hostname, same hardware. The point of the exercise was to find out whether
the rest of this was true.</p>
<p>Host setup is one Ansible playbook: static <abbr>IP</abbr>, <abbr>SSH</abbr> hardening, Docker from the
upstream repo, the shared Docker network, Grafana Alloy for logs and metrics,
and the Komodo Periphery agent. Run it against a freshly flashed card and the
host is ready to be handed stacks.</p>
<p>Two things in it exist only because the rebuild taught me they were needed:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">yaml</span></figcaption><pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span>
    Periphery waits for the network (it does not retry a failed first connect)
  <span class="token key atrule">ansible.builtin.copy</span><span class="token punctuation">:</span>
    <span class="token key atrule">dest</span><span class="token punctuation">:</span> /etc/systemd/system/periphery.service.d/10<span class="token punctuation">-</span>network<span class="token punctuation">-</span>online.conf
    <span class="token key atrule">content</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
      [Unit]
      Wants=network-online.target
      After=network-online.target</span>

<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Periphery starts on boot (installer does not enable it)
  <span class="token key atrule">ansible.builtin.systemd_service</span><span class="token punctuation">:</span>
    <span class="token key atrule">name</span><span class="token punctuation">:</span> periphery
    <span class="token key atrule">state</span><span class="token punctuation">:</span> started
    <span class="token key atrule">enabled</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
</code></pre></figure>
<p>The agent does not retry its first connection, so on a cold boot it raced the
network and lost, and the installer never enabled the unit. Both are the kind of
detail you discover once and then encode, which is the whole argument for the
playbook.</p>
<h2 id="the-backup-is-the-part-you-test">The backup is the part you test</h2>
<p>Before wiping the Pi I took a Proxmox Backup Server snapshot of the whole host
and marked it protected so no retention job could eat it.</p>
<p>Then I did the thing that matters: mounted the snapshot and compared checksums
of the files I actually cared about — the Docker volumes, the Traefik
<code>acme.json</code>, the config directories — against the live host. A backup job that
exits 0 tells you a job ran. Reading the bytes back and finding them identical
tells you that you have a backup.</p>
<p>They matched, and the restore afterwards was uneventful. Certificates came back
without being reissued, and the Apple <abbr>TV</abbr> pairing in an anonymous volume survived
because I had gone looking for it beforehand.</p>
<h2 id="what-i-would-tell-past-me">What I would tell past me</h2>
<p><strong>State that lives in a <abbr>UI</abbr> does not exist.</strong> The single biggest improvement was
moving stack definitions out of a management tool's database and into a git repo
that the management tool reads. The tool became replaceable, which is exactly
what you want from a tool.</p>
<p><strong>Write the migration down before running it.</strong> My reinstall was a document with
numbered phases, a check per service, and a rollback per step. It turned a scary
evening into a checklist, and when something did go wrong I knew which step I
was on.</p>
<p><strong>Keep an explicit list of what is not in code.</strong> The router's port forwards,
<abbr>DHCP</abbr> reservations, the <abbr>CA</abbr>'s provisioner, the Home Assistant configuration, the
Komodo variables, the <abbr>DNS</abbr> records at two registrars. None of it is in the repo,
so the repo documents where each one lives. That list is the real disaster
recovery plan.</p>
<p><strong>Delete the experiments.</strong> My repo had Kubernetes manifests for a cluster that
no longer exists, Ceph notes for a thumb drive I pulled out a year ago, and
compose files for four services I stopped running. Every one of them was a trap
for future me, reading a file and assuming it described reality. Homeprod means
the repo contains what runs, and nothing else — the experiments live in a tag if
I ever want them back.</p>
<p>The lab was more fun. This is better.</p>]]></content:encoded>
            <author>me@kkoppa.com (Kashyap Kopparam)</author>
        </item>
        <item>
            <title><![CDATA[How to replace a book disk in Proxmox]]></title>
            <link>https://kashlabs.com/articles/proxmox-hot-swap-boot-disk/</link>
            <guid>https://kashlabs.com/articles/proxmox-hot-swap-boot-disk/</guid>
            <pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>A Proxmox <abbr>VE</abbr> node may be running 24 hours a day for years. This might lead to
the boot disk wearing out and in need or replacement. This guide lets you swap
the drive out for a new one without data loss.</p>
<p>Note that this method works if you instlled Proxmox using <abbr>ZFS</abbr>.</p>
<h2 id="setup">Setup</h2>
<p>Get your new drive ready, and make sure it is the same size or larger than the
old one. Plug in your drive into an empty bay in the Proxmox machine, or attach
it with an enclosure using <abbr>USB</abbr>. The idea is to have both drives available to
Proxmox.</p>
<p>Let's assume</p>
<p>Old drive: <code>/dev/sda</code> New drive: <code>/dev/sdb</code></p>
<h2 id="step-1-prepare-partitions-on-the-new-drive">Step 1: Prepare partitions on the new drive</h2>
<p>The easy way to do this is to copy over the partitions using <code>sfdisk</code></p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash">sfdisk -d /dev/sda <span class="token operator">&gt;</span> pve-boot-partitions.txt
</code></pre></figure>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token function">cat</span> pve-boot-partitions.txt

label: gpt
label-id: FDB673E1-0941-43AB-BB02-B8A6C3D0D0F8
device: /dev/sda
unit: sectors
first-lba: <span class="token number">34</span>
last-lba: <span class="token number">122142686</span>
sector-size: <span class="token number">512</span>

/dev/sad1 <span class="token builtin class-name">:</span> <span class="token assign-left variable">start</span><span class="token operator">=</span>          <span class="token number">34</span>, <span class="token assign-left variable">size</span><span class="token operator">=</span>        <span class="token number">2014</span>, <span class="token assign-left variable">type</span><span class="token operator">=</span><span class="token number">21686148</span>-6449-6E6F-744E-656564454649, <span class="token assign-left variable">uuid</span><span class="token operator">=</span>4CC31FA5-A6FC-42DB-9071-DF76FBA08F60
/dev/sad2 <span class="token builtin class-name">:</span> <span class="token assign-left variable">start</span><span class="token operator">=</span>        <span class="token number">2048</span>, <span class="token assign-left variable">size</span><span class="token operator">=</span>     <span class="token number">1048576</span>, <span class="token assign-left variable">type</span><span class="token operator">=</span>C12A7328-F81F-11D2-BA4B-00A0C93EC93B, <span class="token assign-left variable">uuid</span><span class="token operator">=</span>B0560706-84DB-45B1-AF0D-BB32DD7F4476
/dev/sda3 <span class="token builtin class-name">:</span> <span class="token assign-left variable">start</span><span class="token operator">=</span>     <span class="token number">1050624</span>, <span class="token assign-left variable">size</span><span class="token operator">=</span>   <span class="token number">121085953</span>, <span class="token assign-left variable">type</span><span class="token operator">=</span>6A898CC3-1DD2-11B2-99A6-080020736631, <span class="token assign-left variable">uuid</span><span class="token operator">=</span>D8BA81D1-0C84-49A8-99B6-F32C041A2FF3
</code></pre></figure>
<p>This is also the input to <code>sfdisk</code>. By modifying the following in this file, it
can be used to create partitions on the new drive.</p>
<ol>
<li>Remove last-blk line. This is tell <code>sfdisk</code> to use the entire disk.</li>
<li>Remove UUIDs for all partition. This will let <code>sfdisk</code> generate new UUIDs for
the new drive.</li>
<li>Replace size with <code>-</code> for the last partition. This will let <code>sfdisk</code> use the
remaining space on the disk for the last partition.</li>
</ol>
<p>It should look like this:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># cat pve-boot-partitions.txt</span>

label: gpt
label-id: FDB673E1-0941-43AB-BB02-B8A6C3D0D0F8
device: /dev/sda
unit: sectors
first-lba: <span class="token number">34</span>
sector-size: <span class="token number">512</span>

/dev/sad1 <span class="token builtin class-name">:</span> <span class="token assign-left variable">start</span><span class="token operator">=</span>          <span class="token number">34</span>, <span class="token assign-left variable">size</span><span class="token operator">=</span>        <span class="token number">2014</span>, <span class="token assign-left variable">type</span><span class="token operator">=</span><span class="token number">21686148</span>-6449-6E6F-744E-656564454649
/dev/sad2 <span class="token builtin class-name">:</span> <span class="token assign-left variable">start</span><span class="token operator">=</span>        <span class="token number">2048</span>, <span class="token assign-left variable">size</span><span class="token operator">=</span>     <span class="token number">1048576</span>, <span class="token assign-left variable">type</span><span class="token operator">=</span>C12A7328-F81F-11D2-BA4B-00A0C93EC93B
/dev/sda3 <span class="token builtin class-name">:</span> <span class="token assign-left variable">start</span><span class="token operator">=</span>     <span class="token number">1050624</span>, <span class="token assign-left variable">size</span><span class="token operator">=</span>   -, <span class="token assign-left variable">type</span><span class="token operator">=</span>6A898CC3-1DD2-11B2-99A6-080020736631
</code></pre></figure>
<p>Use this file to create the partitions on the new drive:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># sfdisk /dev/sdb &lt; pve-boot-partitions.txt</span>
</code></pre></figure>
<p>Check that the partitions are created correctly:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># lsblk /dev/sda /dev/sdb</span>
</code></pre></figure>
<p>Learn more about the partitions in Proxmox in the
<a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#sysboot_installer_part_scheme">Proxmox documentation</a>.</p>
<h2 id="step-2-copy-the-partitions">Step 2: Copy the partitions</h2>
<p>Now that the partitions are created, we can copy the data.</p>
<p>First up is the <abbr>BIOS</abbr> boot partition.</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># dd if=/dev/sda1 of=/dev/sdb1</span>
</code></pre></figure>
<p>Next, the <abbr>EFI</abbr> system partition.</p>
<p>Create the filesystem on the new drive, by simply copying over from the old
drive</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># dd if=/dev/sda2 of=/dev/sdb2</span>
</code></pre></figure>
<p>Check if you are using <abbr>EFI</abbr> or Grub bootloader</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># proxmox-boot-tool status</span>
</code></pre></figure>
<p>If you are using <abbr>EFI</abbr>, you can skip the grub at the end of the command.</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash">proxmox-boot-tool init /dev/sdb2 <span class="token punctuation">[</span>grub<span class="token punctuation">]</span>
</code></pre></figure>
<h2 id="step-3-create-a-mirrored-zfs-pool">Step 3: Create a mirrored ZFS pool</h2>
<p>The idea is to create a mirrored <abbr>ZFS</abbr> pool with the two drives and detach the old
drive.</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># zpool replace -f rpool /dev/sda3 /dev/sdb3</span>
</code></pre></figure>
<p>This will start the resilvering process, which will copy the data from the old
drive to the new drive. You can check the status of the resilvering process
with:</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash"><span class="token comment"># zpool status rpool</span>
</code></pre></figure>
<p>Once the resilvering is complete, you can boot from the new drive.</p>
<h2 id="conculsion">Conculsion</h2>
<p>Always use <abbr>ZFS</abbr> during installation of Proxmox. By using <abbr>ZFS</abbr>, you can easily
replace drives without data loss.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://pve.proxmox.com/wiki/ZFS_on_Linux">Proxmox <abbr>VE</abbr>—<abbr>ZFS</abbr> on Linux</a></li>
<li><a href="https://openzfs.github.io/openzfs-docs/man/master/8/zpool-replace.8.html"><abbr>ZFS</abbr>—zpool replace</a></li>
</ul>]]></content:encoded>
            <author>me@kkoppa.com (Kashyap Kopparam)</author>
        </item>
        <item>
            <title><![CDATA[Code reviews: Thinking of the other side is key]]></title>
            <link>https://kashlabs.com/articles/code-review-tips/</link>
            <guid>https://kashlabs.com/articles/code-review-tips/</guid>
            <pubDate>Mon, 08 May 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>When doing code reviews, it is critical to think of the other side of the
review. This article shows some tips on how that can be achieved with a simple
guideline.</p>
<p>Typically, you are going to submit code and review code.</p>
<h2 id="as-a-reviewer">As a reviewer</h2>
<h3>Categorised comments</h3>
<p>Categorize the comments based on their importance. This will help the author to
prioritize the comments and take action accordingly.</p>
<h4>Question</h4>
<p>A Question might not be actionable in terms of updating the code.</p>
<p>But it is important to get the author's attention, so that you as a reviewer and
peers who might look at the pull request understand better.</p>
<h4>Suggestion</h4>
<p>A Suggestion also might not be actionable in terms of updating the code. It
could be something that is stylistic or something that is not a blocker for the
pull request to be merged.</p>
<p>However, it is important to overcommunicate why a suggesion is made, along with
an example. Eg, this if condition could be more readable if it is extracted to a
variable/function.</p>
<h4>Action required</h4>
<p>These comments require action before the pull request can be merged. It could be
a bug, a security issue, or a performance issue which might impact user
experience or other key metrics of the application.</p>
<p>Again, explaining how the issue might impact users or the application is
important.</p>
<h4>Blocking approval</h4>
<p>These comments are blockers for the pull request to be merged. For example, then
the pull request does not have tests, or the code written is a duplicate and the
author can reuse an existing function/library to do the same.</p>
<p>Another case, is when the code becomes irrelevant due to changing requirements.</p>
<h4>Require offline communication</h4>
<p>This is a special case where the reviewer, the author, and maybe more team
members need to have a discussion offline to come to a consensus.</p>
<p>This could be a design decision, or a decision to refactor a large part of the
codebase. In other cases, it could be a complex feature that needs to be broken
down into smaller pieces or to discuss some missing pieces only discovered after
writing the code.</p>
<h3>Ensure to approve or reject the pull request</h3>
<p>Do not leave the pull request hanging. As long as there is no blocking approval
or require offline communication comment, the pull request should be approved.
We trust that the author to make the changes required, and reply to relevant
comments before they merge the code.</p>
<p>If we fail to approve or reject the pull request, the author will be slowed down
and in turn the team's performance is impacted.</p>
<h2 id="as-an-author">As an author</h2>
<p>Work in progress</p>]]></content:encoded>
            <author>me@kkoppa.com (Kashyap Kopparam)</author>
        </item>
        <item>
            <title><![CDATA[Using WebClient with mTLS in Spring Boot]]></title>
            <link>https://kashlabs.com/articles/spring-boot-web-client-mtls/</link>
            <guid>https://kashlabs.com/articles/spring-boot-web-client-mtls/</guid>
            <pubDate>Wed, 26 Apr 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Going through the documentation of WebClient, Spring Boot and openssl can be
overwhelming. This article is a step-by-step guide to using WebClient with mTLS
in Spring Boot.</p>
<p>We will use openssl to create a keystore in <abbr>PKCS12</abbr> format from certificate and
key files in pem format, and use this to configure WebClient to use mTLS.</p>
<h2 id="setup">Setup</h2>
<p>Obtain the client certificate and key, this will usually be in pem format.</p>
<p>Now we can convert this into a <abbr>PKCS12</abbr> keystore. This keystore can hold multiple
certificates and keys, identified by an alias/friedly name. The whole keystore
is password protected, and each key entry may also be optionally password
protected.</p>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">bash</span></figcaption><pre class="language-bash"><code class="language-bash">openssl pkcs12 -in client_cert.pem -inkey client_key.pem -export -out client.p12 -name my-client
</code></pre></figure>
<h2 id="configure-webclient">Configure WebClient</h2>
<figure class="kl-code kl-wide"><figcaption><span class="kl-tape">kotlin</span></figcaption><pre class="language-kotlin"><code class="language-kotlin"><span class="token keyword">val</span> keyStore <span class="token operator">=</span> KeyStore<span class="token punctuation">.</span><span class="token function">getInstance</span><span class="token punctuation">(</span><span class="token string-literal singleline"><span class="token string">"PKCS12"</span></span><span class="token punctuation">)</span>

<span class="token comment">// read a file as inputStream from path /etc/ssl/certs/client.p12</span>
<span class="token function">File</span><span class="token punctuation">(</span><span class="token string-literal singleline"><span class="token string">"/etc/ssl/certs/client.p12"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">inputStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">use</span> <span class="token punctuation">{</span> inputStream <span class="token operator">-&gt;</span>
    keyStore<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span>inputStream<span class="token punctuation">,</span> <span class="token string-literal singleline"><span class="token string">"password"</span></span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>

<span class="token comment">// read the client certificate and key from the keystore using `my-client` alias</span>
<span class="token keyword">val</span> clientCrt<span class="token operator">:</span> X509Certificate <span class="token operator">=</span> keyStore<span class="token punctuation">.</span><span class="token function">getCertificate</span><span class="token punctuation">(</span><span class="token string-literal singleline"><span class="token string">"my-client"</span></span><span class="token punctuation">)</span> <span class="token keyword">as</span> X509Certificate
<span class="token keyword">val</span> keyStoreEntry <span class="token operator">=</span> keyStore<span class="token punctuation">.</span><span class="token function">getEntry</span><span class="token punctuation">(</span>
    <span class="token string-literal singleline"><span class="token string">"my-client"</span></span><span class="token punctuation">,</span>
    KeyStore<span class="token punctuation">.</span><span class="token function">PasswordProtection</span><span class="token punctuation">(</span><span class="token string-literal singleline"><span class="token string">"password"</span></span><span class="token punctuation">.</span><span class="token function">toCharArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span>
<span class="token keyword">val</span> clientKey <span class="token operator">=</span> <span class="token punctuation">(</span>keyStoreEntry <span class="token keyword">as</span> KeyStore<span class="token punctuation">.</span>PrivateKeyEntry<span class="token punctuation">)</span><span class="token punctuation">.</span>privateKey


<span class="token keyword">val</span> httpClient<span class="token operator">:</span> HttpClient <span class="token operator">=</span>
    HttpClient<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">wiretap</span><span class="token punctuation">(</span><span class="token string-literal singleline"><span class="token string">"reactor.netty.http.client.HttpClient"</span></span><span class="token punctuation">,</span> LogLevel<span class="token punctuation">.</span>DEBUG<span class="token punctuation">,</span> AdvancedByteBufFormat<span class="token punctuation">.</span>TEXTUAL<span class="token punctuation">)</span>
        <span class="token punctuation">.</span><span class="token function">secure</span> <span class="token punctuation">{</span> builder <span class="token operator">-&gt;</span>
            <span class="token comment">// Use this even though it is deprecated so that the TLS version can be negotiated and changed during runtime</span>
            builder<span class="token punctuation">.</span><span class="token function">sslContext</span><span class="token punctuation">(</span>
                SslContextBuilder<span class="token punctuation">.</span><span class="token function">forClient</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">clientAuth</span><span class="token punctuation">(</span>ClientAuth<span class="token punctuation">.</span>REQUIRE<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">keyManager</span><span class="token punctuation">(</span>clientKey<span class="token punctuation">,</span> clientCrt<span class="token punctuation">)</span>
            <span class="token punctuation">)</span>
        <span class="token punctuation">}</span>
<span class="token keyword">val</span> httpConnector<span class="token operator">:</span> ClientHttpConnector <span class="token operator">=</span> <span class="token function">ReactorClientHttpConnector</span><span class="token punctuation">(</span>httpClient<span class="token punctuation">)</span>


<span class="token keyword">return</span> WebClient<span class="token punctuation">.</span><span class="token function">builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">clientConnector</span><span class="token punctuation">(</span>httpConnector<span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span><span class="token string-literal singleline"><span class="token string">"https://mtls-secured-server.com"</span></span><span class="token punctuation">)</span>
    <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

</code></pre></figure>]]></content:encoded>
            <author>me@kkoppa.com (Kashyap Kopparam)</author>
        </item>
    </channel>
</rss>