<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Devops on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/devops/</link><description>Recent content in Devops on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Mon, 03 Feb 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>Command chaining with NTFY for long running commands</title><link>https://blog.iankulin.com/command-chaining-with-ntfy-for-long-running-commands/</link><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/command-chaining-with-ntfy-for-long-running-commands/</guid><description>&lt;p&gt;&lt;a href="https://ntfy.sh/"&gt;NTFY&lt;/a&gt; is a great open-source push notification service that&amp;rsquo;s self-hostable or free to use (although I suggest you &lt;a href="https://liberapay.com/ntfy"&gt;pay for it&lt;/a&gt; as I do). I&amp;rsquo;ve written before how I use it with &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;UptimeKuma&lt;/a&gt; for my uptime monitoring, but another common use is just when I&amp;rsquo;m initiating long-running commands and backgrounding them.&lt;/p&gt;
&lt;p&gt;This magic is possible since we can just &lt;code&gt;curl&lt;/code&gt; to send a NTFY notification. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -d &amp;#34;😀 demo push message via NTFY&amp;#34; ntfy.sh/blog_demo
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since I&amp;rsquo;m subscribed to the &amp;ldquo;blog_demo&amp;rdquo; topic in NTFY, this message will be pushed to my phone and watch:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_0056.png" width="640" alt=""&gt;
&lt;p&gt;How I use this is with &amp;lsquo;command chaining&amp;rsquo;. In Linux, you can stack commands together with the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; characters like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir test_dir &amp;amp;&amp;amp; echo &amp;#34;success&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will create the directory, then print &amp;ldquo;success&amp;rdquo; to the shell. I could use it like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nohup rsync -rvits --bwlimit=20 &amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34; ds1_admin@100.78.2.105:&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34; &amp;gt; output.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; curl -d &amp;#34;💾 upload to vm500-kr complete&amp;#34; ntfy.sh/blog_demo &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Both commands will run in the background, and the output of the first command is directed into the &amp;lsquo;output.log&amp;rsquo; file. If the rsync file transfer (that is going to take all night) finishes successfully, then the message saying it&amp;rsquo;s complete will be sent.&lt;/p&gt;
&lt;p&gt;What about if it fails? Well, posix has you covered here too. There&amp;rsquo;s a &lt;code&gt;||&lt;/code&gt; chaining operator that only runs if a command fails.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir invalid/name &amp;amp;&amp;amp; (echo &amp;#34;Directory created successfully.&amp;#34;) || (echo &amp;#34;Failed to create directory.&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the command above, if we already have a directory called &lt;code&gt;invalid&lt;/code&gt;, the &lt;code&gt;mkdir&lt;/code&gt; will work and we&amp;rsquo;ll get the message &amp;ldquo;Directory created successfully.&amp;rdquo;. If &lt;code&gt;invalid&lt;/code&gt; doesn&amp;rsquo;t exist, the command will fail and we&amp;rsquo;ll get the message &amp;ldquo;Failed to create directory.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Note that I&amp;rsquo;ve added some parenthesis - it makes things clearer for the reader, and the command line parser.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s apply this to our slow file transfer:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nohup rsync -rvits --bwlimit=20 &amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34; ds1_admin@100.78.2.105:&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34; &amp;gt; output.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; curl -d &amp;#34;💾 upload to vm500-kr complete&amp;#34; ntfy.sh/blog_demo || curl -d &amp;#34;⚠️ upload to vm500-kr failed!&amp;#34; ntfy.sh/blog_demo &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we&amp;rsquo;ll get a push message for completion or failure. There is one more little bit of housekeeping to do though. When we curl ntfy like this, it actually returns some JSON:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-12-28-at-11.00.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-12-28-at-11.00.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running this whole thing backgrounded, we really want that to go to the &lt;code&gt;output.log&lt;/code&gt; file with the other output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nohup rsync -rvits --bwlimit=20 &amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34; ds1_admin@100.78.2.105:&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34; &amp;gt; output.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; curl -d &amp;#34;💾 upload to vm500-kr complete&amp;#34; ntfy.sh/blog_demo &amp;gt;&amp;gt; output.log || curl -d &amp;#34;⚠️ upload to vm500-kr failed!&amp;#34; ntfy.sh/blog_demo &amp;gt;&amp;gt; output.log &amp;amp;
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Moving a Docker image as a file</title><link>https://blog.iankulin.com/moving-a-docker-image-as-a-file/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-a-docker-image-as-a-file/</guid><description>&lt;p&gt;I&amp;rsquo;m having a super annoying problem at the moment, I can&amp;rsquo;t pull down containers from DockerHub. If I hotspot my laptop off my phone it works fine, so it&amp;rsquo;s some drama with the home internet connection that rebooting the router does not fix.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve had a couple of different errors including &lt;code&gt;Error response from daemon: Get &amp;quot;https://registry-1.docker.io/v2/&amp;quot;: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)&lt;/code&gt; and &lt;code&gt;Error response from daemon: Get &amp;quot;https://registry-1.docker.io/v2/&amp;quot;: dial tcp: lookup registry-1.docker.io&lt;/code&gt;. I can&amp;rsquo;t actually ping &lt;code&gt;registry-1.docker.io&lt;/code&gt; or &lt;code&gt;hub.docker.com&lt;/code&gt;, although I can open hub.docker.com in a browser, so it works for ports 80 and 443, but not some other udp ports.&lt;/p&gt;
&lt;p&gt;Anyway, I needed to update my Jellyfin server as the app on my TV was complaining that it would stop working on the next Android app update if I didn&amp;rsquo;t upgrade to 10.10. Since all my homelab gear is connected through the home internet, I needed a way to download the container to my laptop (connected to my phone), then reconnect to the home network and shift the container to the server.&lt;/p&gt;
&lt;p&gt;Turns out this is no drama.&lt;/p&gt;
&lt;h3 id="what-is-an-oci-image"&gt;What is an OCI image?&lt;/h3&gt;
&lt;p&gt;Before I run through the commands, it&amp;rsquo;s worth appreciating exactly what a container is. It&amp;rsquo;s comprised of &lt;em&gt;layers&lt;/em&gt; - you will have noticed this pulling or building them. And if you already have a layer (perhaps you&amp;rsquo;re pulling two containers based on debian:stable) it will only download that layer once, and the second time it will find it via the giant sha256 hex string and reuse it. Of course if we have all these layers hanging around (don&amp;rsquo;t worry about exactly where - it&amp;rsquo;s abstracted away for us) you need some sort of document that says which exact layers in what order make up the container image. We could call that the manifest.&lt;/p&gt;
&lt;p&gt;So if we wanted to export an image, it would basically be a collection of binaries named with those sha256 names, and a manifest that describes how to rebuild it. We&amp;rsquo;ll see later it&amp;rsquo;s a tiny bit more complex than that, but not by much.&lt;/p&gt;
&lt;h3 id="commands"&gt;Commands&lt;/h3&gt;
&lt;p&gt;First you need to have pulled the image down from the repository, so when you list your images with &lt;code&gt;docker image ls&lt;/code&gt; you can see it in the list. In my case, since I was working on an M1 (ARM) MacBook and wanted the Linux/64 image (to run on my Debian VMs) I had to specify the platform I needed from the multi-architecture image&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker pull --platform linux/amd64 jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once it&amp;rsquo;s pulled down, we output it to a file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker save -o jellyfin.image jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;jellyfin.image&lt;/code&gt; is just what I&amp;rsquo;m calling my file, it could be anything. In fact, lets call it &lt;code&gt;jellyfin.tar&lt;/code&gt;, that will be more fun.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker save -o jellyfin.tar jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Only because it actually is a zipped up file. You can probably guess what&amp;rsquo;s going to be in it:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.02.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.02.28-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Yep - a folder of layer binaries named with their sha256s, and a manifest file saying how to put it together.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve got your image in it&amp;rsquo;s file, you move it to the machine where you need it, then make it available to docker there with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker load -i jellyfin.image
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that&amp;rsquo;s done, it will be available with it&amp;rsquo;s original name and tag, and you&amp;rsquo;re good to go.&lt;/p&gt;</description></item><item><title>Updating a deployment on fly.io</title><link>https://blog.iankulin.com/updating-a-deployment-on-fly-io/</link><pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/updating-a-deployment-on-fly-io/</guid><description>&lt;img src="https://blog.iankulin.com/images/flyio_picture.png" width="620" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve had my external UptimeKuma chugging away on &lt;a href="https://fly.io/"&gt;fly.io&lt;/a&gt;, for free, for months now, and the container image it was based on was a bit out of date, so I wanted to update it. I hadn&amp;rsquo;t looked at fly.io for months, and couldn&amp;rsquo;t really recall what I&amp;rsquo;d done to create it.&lt;/p&gt;
&lt;p&gt;The way this works is that that you create a fly.toml file that sets out the details of your app. From memory I think I used the one from the docs and gave it a unique name, the name of the Docker image, the port, the datacentre location, and the directory for the persisted data. The you run &lt;code&gt;fly deploy&lt;/code&gt; from the directory with the toml file (having already installed the CLI tool and logged in) and you&amp;rsquo;re in business.&lt;/p&gt;
&lt;p&gt;Fly doesn&amp;rsquo;t actually run your container, it deconstructs it and uses the layer information to launch a firecracker instance, but of course, none of this matters to the user - it&amp;rsquo;s just as if your containerised app is magically live on the internet with hardly any effort or money (so far I&amp;rsquo;ve paid $0.00 for eight months of good service).&lt;/p&gt;
&lt;p&gt;I was sort of dreading the upgrade, I guessed I&amp;rsquo;d need to kill the old instance, start the new one and connect it back to my persistent storage, but here&amp;rsquo;s what I actually did.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Went to the folder with my fly.toml file&lt;/li&gt;
&lt;li&gt;Typed &lt;code&gt;fly deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-28-at-6.22.46-am-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-28-at-6.22.46-am-1.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Fly.io is such a great way to deploy stuff. If I wasn&amp;rsquo;t such a committed self-hoster I would use it a lot more. They used to be hosted on Heroku (which is on AWS) but I understand they have moved to their own worldwide data centers. Their secret sauce is the dev experience. So good.&lt;/p&gt;
&lt;h3 id="edit-update-from-the-future"&gt;Edit: Update from the future&lt;/h3&gt;
&lt;p&gt;So, very day or so since I did that update, which was to version 1.23, I&amp;rsquo;ve been getting these emails from Fly.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Fly.io] ikuptime ran out of memory and crashed
Fly &amp;lt;support@fly.io&amp;gt;
	
4:12 AM (16 hours ago)
	

Hello! Your “ikuptime” application hosted on Fly.io crashed because it ran out of memory. Specifically, the instance 3d8d7de3b20738. Adding more RAM to your application might help!
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hmm. In theory the free machine I&amp;rsquo;m using on Fly includes 256MB of RAM, and when I look at the average use, it&amp;rsquo;s sitting around 200MB, but it does say out of 223MB, so I guess that&amp;rsquo;s the real limit.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.37.18-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.37.18-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Looking at the graph of memory use, it does look like there&amp;rsquo;s something in the container with a memory leak, then it&amp;rsquo;s being restarted once it hits about 205MB for a while.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.31.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.31.28-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;An easy fix might be to swap to a lighter weight container. You can see at the end of the graph above I&amp;rsquo;ve dropped it down to about 160MB. That was by using the image tagged with &lt;code&gt;:1-alpine&lt;/code&gt;. I&amp;rsquo;ll keep and eye on it and see what happens.&lt;/p&gt;
&lt;p&gt;I am running the full size container in the home lab inside an LXC, and it doesn&amp;rsquo;t seem to have the leak.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.40.49-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-06-at-8.40.49-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is not quite an apples for apples comparison. Fly.io doesn&amp;rsquo;t actually run the container, it uses the container layers to build the app in a tiny VM called &lt;a href="https://firecracker-microvm.github.io/"&gt;firecracker&lt;/a&gt;. This is the technology used by AWS to run serverless functions.&lt;/p&gt;
&lt;p&gt;I guess I&amp;rsquo;ll be able to see in a day or so if I&amp;rsquo;ve solved the problem.&lt;/p&gt;
&lt;h3 id="edit-update-from-the-distant-future"&gt;Edit: Update from the distant future&lt;/h3&gt;
&lt;p&gt;Perhaps the memory growth is still there (after an update it drops down 12ish MB):&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-03-17-at-12.56.40.png" width="900" alt=""&gt;
&lt;p&gt;but in any case, running the Alpine base image has kept the memory use well under the limits for my free instance.&lt;/p&gt;
&lt;p&gt;In other news, I was on a new laptop when I tried to run the &lt;code&gt;fly deploy&lt;/code&gt; command today, so things were a tiny bit more complex. I had to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;install the fly command line stuff with &lt;code&gt;brew install flyctl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;then when I ran &lt;code&gt;fly deploy,&lt;/code&gt; it asked me to sign in, and opened a web page for me to do so.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Controlling Docker container startup order</title><link>https://blog.iankulin.com/controlling-docker-container-startup-order/</link><pubDate>Mon, 02 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/controlling-docker-container-startup-order/</guid><description>&lt;p&gt;A very common scenario when running services in Docker containers is that one service is going to depend on another. The most common example is going to be if you have a service that needs a database - you&amp;rsquo;re going to want the container running the database to be ready for business before the service that needs it starts. And conversely, when you shut things down, you want to stop the service before you kill the database or you may lose some data.&lt;/p&gt;
&lt;p&gt;Both of these things are easily catered for with containers started with docker compose, but there&amp;rsquo;s a few caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The services need to share a docker-compose file&lt;/li&gt;
&lt;li&gt;The services need to share a network (which they will by default if they&amp;rsquo;re in the same compose file and you don&amp;rsquo;t override it)&lt;/li&gt;
&lt;li&gt;The service that is depended on, must telegraph it&amp;rsquo;s state with a health check.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="application"&gt;Application&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been doing monitoring by running an app (&lt;a href="https://github.com/IanKulin/vitals-glimpse"&gt;vitals-glimpse&lt;/a&gt;) on all my services &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;that exposes some very basic metrics&lt;/a&gt; as an API endpoint. Then a couple of instances of UptimeKuma (one on fly.io, for monitoring outside services and one inside the homelab network) monitor all those, and check an okay flag for &lt;em&gt;up&lt;/em&gt; vs &lt;em&gt;down&lt;/em&gt;. If something changes status, I get an &lt;a href="https://ntfy.sh/"&gt;ntfy&lt;/a&gt; message on my watch.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.15.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.15.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a great setup, and I&amp;rsquo;ll be keeping it, but I have &lt;a href="https://grafana.com/"&gt;Graphana&lt;/a&gt; envy, so I need to be grabbing those values and saving them in a time series database. There&amp;rsquo;s not much thought needed to be put into which database, it&amp;rsquo;s &lt;a href="https://www.influxdata.com/"&gt;InfluxDB&lt;/a&gt;. As for pulling in all the data, there&amp;rsquo;s probably a highly configurable open source solution for this, or, I could just write my own and call it &lt;a href="https://github.com/IanKulin/glimpse-scan"&gt;glimpse-scan&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But now I need to run glimpse-scan and InfluxDB in containers together, and have glimpse-scan wait for Influx to be ready before it gets to work. We&amp;rsquo;re going to do that with docker-compose.&lt;/p&gt;
&lt;h2 id="healthcheck"&gt;healthcheck&lt;/h2&gt;
&lt;p&gt;For this to work, there needs to be some CLI command you can run to check the health of the service we&amp;rsquo;re going to wait on. If the service was a website, you might just curl the index page like &lt;code&gt;curl http://localhost&lt;/code&gt; and since InfluxDB actually does contain a little web server that would work, but they actually provide a better one:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;curl -f http://localhost:8086/ping&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Having some sort of health check like this is super common for containerised databases so some googling will find what you&amp;rsquo;re after. The command, what ever it is, needs to return an error code if it&amp;rsquo;s not successful. This is almost universal for unix commands.&lt;/p&gt;
&lt;p&gt;Now we just add that into the compose file.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 influxdb:
 image: influxdb:2 
 container_name: influxdb
 healthcheck:
 test: [&amp;#34;CMD-SHELL&amp;#34;, &amp;#34;curl -f http://localhost:8086/ping&amp;#34;]
 interval: 5s
 timeout: 10s
 retries: 5
 ports:
 - &amp;#34;8086:8086&amp;#34;
 environment:
 - DOCKER_INFLUXDB_INIT_MODE=setup
 - DOCKER_INFLUXDB_INIT_USERNAME=${INFLUXDB_ADMIN_USER}
 - DOCKER_INFLUXDB_INIT_PASSWORD=${INFLUXDB_ADMIN_PASSWORD}
 - DOCKER_INFLUXDB_INIT_ORG=${INFLUXDB_ORG}
 - DOCKER_INFLUXDB_INIT_BUCKET=${INFLUXDB_BUCKET}
 - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${INFLUXDB_ADMIN_TOKEN}
 - INFLUXD_METRICS_DISABLED=true
 volumes:
 - ./influxdb/data:/var/lib/influxdb2
 restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even if you don&amp;rsquo;t need your containers to depend on one another, it might still be a good idea to add health checks like this since it makes the &lt;code&gt;docker ps&lt;/code&gt; information a bit more helpful.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.41.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.41.53-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="depends_on"&gt;depends_on&lt;/h2&gt;
&lt;p&gt;The next step is to tell the other container (in our example, the glimpse-scan app) which other service it depends on.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; glimpse-scan:
 image: ghcr.io/iankulin/glimpse_scan:latest
 container_name: glimpse-scan
 depends_on:
 influxdb:
 condition: service_healthy
 build:
 context: .
 dockerfile: Dockerfile
 volumes:
 - ./glimpse-scan/data:/app/data
 restart: unless-stopped
 env_file:
 - .env
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="go-time"&gt;Go time&lt;/h2&gt;
&lt;p&gt;And, compose it up (remember the two lots of code above are together in a single &lt;code&gt;docker-compose.yml&lt;/code&gt; file).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.43-pm.png" width="906" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.47-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.47-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.52-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-20-at-4.46.52-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Fixing TLS for wget in BusyBox</title><link>https://blog.iankulin.com/fixing-tls-for-wget-in-busybox/</link><pubDate>Mon, 25 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fixing-tls-for-wget-in-busybox/</guid><description>&lt;p&gt;I&amp;rsquo;ve been containerising my static websites with BusyBox (because it&amp;rsquo;s small), and in &lt;a href="https://blog.iankulin.com/fancier-website-in-a-docker-container/"&gt;an earlier post&lt;/a&gt; showed how to even get the container to update parts of the site by reaching out with &lt;code&gt;wget&lt;/code&gt; to download resources from elsewhere and saving them inside the container where we are serving the &amp;lsquo;static&amp;rsquo; site from. I&amp;rsquo;d done this by including a bash script in the container with the &lt;code&gt;wget&lt;/code&gt; in a loop with a &lt;code&gt;sleep&lt;/code&gt;. Then started the script and the httpd server in the CMD line of the &lt;code&gt;dockerfile&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the dockerfile.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busybox:latest

# Add shell script and set executable
COPY update_content.sh /usr/local/bin/update_content.sh
RUN chmod +x /usr/local/bin/update_content.sh

# Create the directory for the web content, and copy files in
RUN mkdir -p /var/www/html
COPY www/. /var/www/html

# Expose port 80 for the web server
EXPOSE 80

# Start the httpd server
CMD [&amp;#34;sh&amp;#34;, &amp;#34;-c&amp;#34;, &amp;#34;/usr/local/bin/update_content.sh &amp;amp; busybox httpd -f -p 80 -h /var/www/html&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And the bash script:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/sh

# Define the URL and the destination path
URL=&amp;#34;http://httpbin.org/image/jpeg&amp;#34;
DEST_PATH=&amp;#34;/var/www/html/image.jpg&amp;#34;
FETCH_INTERVAL=120 # 2 minutes

while true; do
 # Use wget to download the file
 wget -O &amp;#34;$DEST_PATH&amp;#34; &amp;#34;$URL&amp;#34;

 # Check the exit status of wget
 if [ $? -eq 0 ]; then
 echo &amp;#34;File downloaded successfully to $DEST_PATH&amp;#34;
 else
 echo &amp;#34;Failed to download the file.&amp;#34;
 fi
 sleep $FETCH_INTERVAL
done
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This all works perfectly, as long as the file you&amp;rsquo;re downloading is over http. Trying over an SSL connection (which must be 98% of the internet by now) fails. The reason for this is that &lt;a href="https://blog.iankulin.com/fancier-website-in-a-docker-container/"&gt;BusyBox does not contain the certificates for the root CA&lt;/a&gt;. In a normal distribution, you&amp;rsquo;d just do ahead and install them, but BusyBox also does not have a package manager to help you do that, so there&amp;rsquo;s no &amp;lsquo;&lt;code&gt;apk update &amp;amp;&amp;amp; apk add ca-certificates&lt;/code&gt;&amp;rsquo; to help us out.&lt;/p&gt;
&lt;p&gt;A viable solution might be to just switch to an Alpine container, but I&amp;rsquo;d be going up to 12MB per containerised website then (from 4) which seems a bit much.&lt;/p&gt;
&lt;p&gt;In a &lt;a href="https://blog.iankulin.com/fancier-website-in-a-docker-container/"&gt;Stack Overflow post&lt;/a&gt;, &lt;a href="https://stackoverflow.com/users/2830850/tarun-lalwani"&gt;Tarun Lalwani&lt;/a&gt; offers a couple of suggestions. One is having a multi-stage docker image build where you create an Alpine container, copy the certs out to a volume, then copy them into your busybox image. To my mind that would be a good idea to create a new image (essentially BusyBox with certs) to chuck up on a repository somewhere. Such an &lt;a href="https://hub.docker.com/r/odise/busybox-curl"&gt;image does exist,&lt;/a&gt; but it&amp;rsquo;s very old.&lt;/p&gt;
&lt;p&gt;Another suggestion is just to bind mount the certs directory in the container to the host (as read only), and use the host&amp;rsquo;s certificates. This seems like a much simpler approach to me. It&amp;rsquo;s just an edit to the &lt;code&gt;docker-compose.yml&lt;/code&gt; or the run command.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 example.com:
 container_name: httpd-example.com
 image: ghcr.io/iankulin/example.com:latest
 restart: unless-stopped
 volumes:
 - /etc/ssl/certs:/etc/ssl/certs:ro # Bind mount host&amp;#39;s SSL certs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run --name httpd-example.com -p 80:80 -v /etc/ssl/certs:/etc/ssl/certs:ro ghcr.io/iankulin/example.com:latest
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Fancier Website in a Docker Container</title><link>https://blog.iankulin.com/fancier-website-in-a-docker-container/</link><pubDate>Mon, 18 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fancier-website-in-a-docker-container/</guid><description>&lt;p&gt;The previous post went over how to bundle a static website into a Docker container. That&amp;rsquo;s a neat little trick - keeping the entire website and making it trivial to install on a VPS behind Nginx Proxy Manager. It worked great for most of my little websites.&lt;/p&gt;
&lt;h3 id="but"&gt;But&amp;hellip;&lt;/h3&gt;
&lt;p&gt;A couple of my websites had very minor &amp;lsquo;dynamic&amp;rsquo; content. One was pulling down the local temperature from OpenWeather, then exposing a cut-down version of that as a REST endpoint so all my servers could grab it without me being rate-limited by OpenWeather for abusing my free API key. Another one re-hosted an image that changes a couple of times a day from an unreliable service.&lt;/p&gt;
&lt;p&gt;So, can we do those sorts of jobs in our BusyBox web containers? Well yes, of course. Let&amp;rsquo;s look at the image re-hosting problem, but the approach is going to be similar for other small internet tasks.&lt;/p&gt;
&lt;p&gt;We need the container (as well as hosting the website) to repeatedly download an image from the internet, and save it into the directory in the container where the static files are being hosted. In my first attempt at this, I messed around with cron, but I was over complicating it, and since BusyBox is not a full distro with all the regular tools, lots of things (including cron) just didn&amp;rsquo;t work the way I expected the first try.&lt;/p&gt;
&lt;p&gt;Where I ended up was having a script called &lt;code&gt;update_content.sh&lt;/code&gt; that has a loop with a &lt;code&gt;sleep()&lt;/code&gt; in the bottom, but at the top, downloads the file we want into our &lt;code&gt;/var/www/html/&lt;/code&gt; directory. Here&amp;rsquo;s the script:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/sh

# Define the URL and the destination path
URL=&amp;#34;http://httpbin.org/image/jpeg&amp;#34;
DEST_PATH=&amp;#34;/var/www/html/image.jpg&amp;#34;
FETCH_INTERVAL=120 # 2 minutes

while true; do
 # Use wget to download the file
 wget -O &amp;#34;$DEST_PATH&amp;#34; &amp;#34;$URL&amp;#34;

 # Check the exit status of wget
 if [ $? -eq 0 ]; then
 echo &amp;#34;File downloaded successfully to $DEST_PATH&amp;#34;
 else
 echo &amp;#34;Failed to download the file.&amp;#34;
 fi
 sleep $FETCH_INTERVAL
done
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in our &lt;code&gt;dockerfile&lt;/code&gt;, the &lt;code&gt;CMD&lt;/code&gt; launches the script as well as the httpd server:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busybox:latest

# Add shell script and set executable
COPY update_content.sh /usr/local/bin/update_content.sh
RUN chmod +x /usr/local/bin/update_content.sh

# Create the directory for the web content, and copy files in
RUN mkdir -p /var/www/html
COPY www/. /var/www/html

# Expose port 80 for the web server
EXPOSE 80

# Start the httpd server
CMD [&amp;#34;sh&amp;#34;, &amp;#34;-c&amp;#34;, &amp;#34;/usr/local/bin/update_content.sh &amp;amp; busybox httpd -f -p 80 -h /var/www/html&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The docker-compose.yml remains the same as in the previous post - if you haven&amp;rsquo;t read that, I was running all these website containers behind Nginx Proxy Manager. If you are not, then just go ahead and delete out the &amp;ldquo;networks&amp;rdquo; parts.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 example.com:
 container_name: httpd-example.com
 image: ghcr.io/iankulin/example.com:latest
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Eagle eyed readers (or people with experience of using the BusyBox version of wget) will have noticed the oddly particular image file I chose to download for this demo code:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;URL=&amp;quot;http://httpbin.org/image/jpeg&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I chose this, because it&amp;rsquo;s one of the last image files in the world to be served over http, and wget in BusyBox chokes on TLS for reasons I&amp;rsquo;ll discuss next week.&lt;/p&gt;</description></item><item><title>Website in a Docker Container</title><link>https://blog.iankulin.com/website-in-a-docker-container/</link><pubDate>Mon, 11 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/website-in-a-docker-container/</guid><description>&lt;p&gt;Having figured out how to use the GitHub package registry, I was a bit inspired by &lt;a href="https://lipanski.com/posts/smallest-docker-image-static-website"&gt;this blog post&lt;/a&gt; from Florin Lipan to deliver all my little static websites as Docker containers. I&amp;rsquo;m not as focused as he is about making them tiny, but I did steal the idea of using &lt;a href="https://busybox.net/about.html"&gt;BusyBox&lt;/a&gt; httpd for serving them, resulting in about 4MB containers. That&amp;rsquo;s small enough for me, and since they are all very similar, there&amp;rsquo;s a fair bit of layer reuse going on.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the setup. I dump the static (html, css, js etc) files for the website into a &amp;lsquo;www&amp;rsquo; sub-directory.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-6.55.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-6.55.02-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The dockerfile pulls in BusyBox then copies those files into the container. Note these are in the container, it&amp;rsquo;s not going to be bound to an external directory (where we could change them), the container carries its website files with it. Any change to the website content will require a container rebuild.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 80&lt;/code&gt; doesn&amp;rsquo;t really do anything, it&amp;rsquo;s pretty much just documentation. Then the &lt;code&gt;CMD&lt;/code&gt; directive starts the server on port 80 and points to the static files that we copied in earlier.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busybox:latest

# Create the directory for the web content, and copy files in
RUN mkdir -p /var/www/html
COPY www/. /var/www/html

# Expose port 80 for the web server
EXPOSE 80

# Start the httpd server
CMD [&amp;#34;sh&amp;#34;, &amp;#34;-c&amp;#34;, &amp;#34;busybox httpd -f -p 80 -h /var/www/html&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To use this dockerfile to build our container, just docker build it and give it a tag:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker build -t ghcr.io/iankulin/example.com:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then if we run it, and go to http://localhost, there&amp;rsquo;s our website.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run --name httpd-example.com -p 80:80 ghcr.io/iankulin/example.com:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="with-nginx-proxy-manager"&gt;With NGINX Proxy Manager&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; file I use on the VPS host is slightly more complicated. We want each of the website containers to run in the same docker network as Nginx Proxy Manager - since docker networks have their own little dns server based on the container names, that&amp;rsquo;s going to make hooking it up trivial.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 example.com:
 container_name: httpd-example.com
 image: ghcr.io/iankulin/example.com:latest
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="architecture"&gt;Architecture&lt;/h3&gt;
&lt;p&gt;Since I develop on an M1 MacBook, but host all my workloads on regular AMD64 Linux LXC containers or VMs, I need to build for that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker build --platform linux/amd64 -t ghcr.io/iankulin/example.com:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In actual fact, I could have built that way for the Mac as well - Docker Desktop would have just run it in a Linux VM with a small performance penalty which wouldn&amp;rsquo;t be noticeable for my purposes. Once it&amp;rsquo;s built, we push it up to the GitHub Container Registry.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker push ghcr.io/iankulin/example.com:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Working with the registry is well covered in my previous post, so I won&amp;rsquo;t go into those details here.&lt;/p&gt;
&lt;h3 id="on-the-host"&gt;On the host&lt;/h3&gt;
&lt;p&gt;On the host where the website is to run, I just make a directory for it and drop the &lt;code&gt;docker-compose.yml&lt;/code&gt; in. Then &lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running in the Nginx Proxy Manager docker network, when we specify the host name for the new web site for NPM to proxy to, it&amp;rsquo;s just the container name we gave it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then the DNS settings for your domain need to be pointed to this host. Once that&amp;rsquo;s propagated, you&amp;rsquo;ll be able to request the SSL certificate in NPM and your website is live.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png" width="886" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Using the GitHub Container Registry</title><link>https://blog.iankulin.com/using-the-github-container-registry/</link><pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-the-github-container-registry/</guid><description>&lt;p&gt;As the number of little projects I&amp;rsquo;m running on VPSs grows, I need to have a regimented system for managing all that. I could be using something like &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt;, but, at least for the moment, I&amp;rsquo;d rather build my own system.&lt;/p&gt;
&lt;p&gt;Currently my system is Nginx Proxy Manager (dockerised) in front of each app. If it&amp;rsquo;s a static website, that&amp;rsquo;s another dockerised Nginx, started with a compose file and with &lt;code&gt;www&lt;/code&gt; and &lt;code&gt;conf&lt;/code&gt; sub-directories that I&amp;rsquo;ve &lt;code&gt;git pull&lt;/code&gt;ed from the project. It&amp;rsquo;s not pretty.&lt;/p&gt;
&lt;p&gt;It occurs to me that I could just be bundling each static website &lt;em&gt;inside&lt;/em&gt; a Docker image, then the only content for each website on the VPS would be a compose file. This has the extra appeal that eventually I could use GitHub CI/CD to rebuild the container so changing a website would be pushing my edits to main, then &lt;code&gt;compose down&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, and &lt;code&gt;up&lt;/code&gt; on the VPS.&lt;/p&gt;
&lt;p&gt;Currently I&amp;rsquo;ve only been using DockerHub for my containers, but the free plan only allows for a single private image, whereas on GitHub the free plan doesn&amp;rsquo;t have a number of packages limit - rather it has a total storage (500MB) and monthly transfers (1GB) &lt;a href="https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-packages/about-billing-for-github-packages"&gt;limits&lt;/a&gt;. Along with the aforementioned integration with GitHub CI/CD, this makes it the obvious place to store these images until my scale is large enough to set up my own registry (which I would probably do with &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; on a VPS since the limits of running that inside my Tailscale network is starting to be a friction point anyway).&lt;/p&gt;
&lt;p&gt;Long story short - I want to start using the GitHub container registry, and this post steps through that.&lt;/p&gt;
&lt;h2 id="access-tokens"&gt;Access Tokens&lt;/h2&gt;
&lt;p&gt;If you set up your Docker Hub a while ago, you&amp;rsquo;ve probably forgotten that early on, you had to log into it from the command line with the &lt;code&gt;docker login&lt;/code&gt; command. With Docker Hub, that&amp;rsquo;s your usual username/password combo, but GitHub has a more fine-grained system of permissions, where you generate &amp;lsquo;Personal Access Tokens&amp;rsquo;. This is quite cool, for example you can generate a token with add/update/delete access for your main development laptop, but then generate a different token that only has read access to use on the server where you need to deploy the image.&lt;/p&gt;
&lt;p&gt;The Personal Access Token (PAT) is just used in place of a password when you log in. As well as the benefit of being able to control the permissions for each PAT when you create it, you also name them. This would be helpful for example if your software lead had their laptop stolen, and you needed to revoke the PAT for that device. It&amp;rsquo;s also possible to set expiry dates for the PATs.&lt;/p&gt;
&lt;p&gt;Generating the PATs is done in:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GitHub | Profile | Settings | Developer Settings (left column, bottom) | Personal Access Tokens | Tokens Classic | Generate Access Token Classic&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you have MFA set up (you should) it will ask for that. Then give it a &amp;lsquo;Note&amp;rsquo; and the permissions you want for &amp;ldquo;Packages&amp;rdquo; - Container images are stored in the &amp;ldquo;Packages&amp;rdquo; section of GitHub (where it&amp;rsquo;s also possible to store other artifacts such as your private NPM packages). In the example below we&amp;rsquo;ve asked for Read/Write/Delete access.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.49.54-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.49.54-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s completed, you&amp;rsquo;ll be shown a list of your existing PATs, plus the new one you&amp;rsquo;ve just generated. This is the only time it will ever be displayed in GitHub, so you need to copy it out to where you need it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.50.11-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-11.50.11-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="logging-in"&gt;Logging in&lt;/h2&gt;
&lt;p&gt;Before we can push an image to the GitHub Container Registry (ghcr.io) we&amp;rsquo;ll need to use this PAT to log in. I&amp;rsquo;m using the command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker login --username &amp;lt;github username&amp;gt; --password &amp;lt;PAT we just generated&amp;gt; ghcr.io
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;ghcr.io&lt;/code&gt; is just the URI for the container registry. It will complain about you pasting the PAT in the command line like that - I guess it&amp;rsquo;s in your bash history now. I&amp;rsquo;ll leave you to google how to do that more securely.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.02.37-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Note that you can be logged into several container registries at once. Logging into the ghcr.io won&amp;rsquo;t mean that you&amp;rsquo;ll need to re-logging to DockerHub later; that will still work fine.&lt;/p&gt;
&lt;h2 id="pushing"&gt;Pushing&lt;/h2&gt;
&lt;p&gt;Once that&amp;rsquo;s done, you can push images just as you are used to with DockerHub, with the exception that you need to specify the container registry as part of your image name. It&amp;rsquo;s possible to do that with &lt;code&gt;hub.docker.com&lt;/code&gt; as well, but Docker privileged themselves to make it a default. To use a different registry (in our case ghcr.io) it needs to be included in the image name, along with your GitHub use name.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker push ghcr.io/&amp;lt;github user name&amp;gt;/&amp;lt;container name&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.08.50-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you head to GitHub, and go into &amp;ldquo;Packages&amp;rdquo; instead of &amp;ldquo;Repositories&amp;rdquo;, your container will be there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.24.10-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.24.10-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="pulling"&gt;Pulling&lt;/h2&gt;
&lt;p&gt;Pulling the container is going to be even simpler, log in to the registry with the same command we used above, then just docker pull with the registry in the container name - exactly as suggested in the package page on GitHub above.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker pull ghcr.io/&amp;lt;github user name&amp;gt;/&amp;lt;container name&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.29.33-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-06-at-12.29.33-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>rsync between Synology NAS</title><link>https://blog.iankulin.com/rsync-between-synology-nas/</link><pubDate>Mon, 30 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-between-synology-nas/</guid><description>&lt;p&gt;A while ago, I devised a complicated system where I could drop files in a web interface running on an LXD container and the files would then magically appear in a directory on a remote NAS in the morning. It turned out to not be very robust, and I gave up on it after a while.&lt;/p&gt;
&lt;p&gt;Also, really there should be no need for it - underneath, it was just using &lt;code&gt;rsync&lt;/code&gt; to move the files, so why not just do that direct from one NAS to another? Well, mainly because my NASs are all Synology - which I love, and they&amp;rsquo;ve been great, but in an effort to make them usable by muggles, Synology tend to somewhat complicate things for Linux command line wizards.&lt;/p&gt;
&lt;p&gt;It turns out to be totally possible to command line &lt;code&gt;rsync&lt;/code&gt;, including doing it over Tailscale, but there&amp;rsquo;s a couple of gotchas along the way.&lt;/p&gt;
&lt;h3 id="rsync-the-synology-way"&gt;rsync the Synology way&lt;/h3&gt;
&lt;p&gt;A reasonable question would be why didn&amp;rsquo;t I use the Synology rsync user interface to do all this - it&amp;rsquo;s right there in Control Panel / File Services? I did actually look at doing that, but after five minutes I couldn&amp;rsquo;t figure it out, so yeah na. It&amp;rsquo;s the command line for me.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;The plan for the rest of this post is just to run through, in approximate order, the steps you&amp;rsquo;ll need to take to get &lt;code&gt;rsync&lt;/code&gt; working from the command line to sync files between two synology NASs. It&amp;rsquo;s probably also helpful between a real system and a Synology NAS. I&amp;rsquo;m going to talk about the &amp;rsquo;local&amp;rsquo; NAS (where we&amp;rsquo;ll be running the rsync command) and &amp;lsquo;remote&amp;rsquo; one. This is just for convenience - you might have two local or two remote NASs - I don&amp;rsquo;t judge. I&amp;rsquo;m just calling mine &amp;rsquo;local&amp;rsquo; and &amp;lsquo;remote&amp;rsquo; for this post that so you know which device I&amp;rsquo;m talking about.&lt;/p&gt;
&lt;h4 id="ssh"&gt;ssh&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt; works over an ssh connection, so you need to be able to ssh from one NAS to another without entering a password first. To test it, ssh into the local NAS, then without logging out, ssh into the remote NAS from the local one. If that works without asking for a password you&amp;rsquo;ve completed this step and can just ctrl-D to drop back to the local NAS.&lt;/p&gt;
&lt;p&gt;If the issue is that it asked for a password, that just means you need to install your public ssh keys on the remote. I usually do this with the &lt;code&gt;ssh-copy-id&lt;/code&gt; command on regular Linux, Mac and BSD systems, but that&amp;rsquo;s not available at the Synology command line so we&amp;rsquo;ll have to do it the old fashioned way.&lt;/p&gt;
&lt;p&gt;Anything to do with ssh is stored in a hidden directory, &lt;code&gt;.ssh&lt;/code&gt; in a user&amp;rsquo;s home directory. For example you can check you&amp;rsquo;ve got public keys with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ls -la ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These are the keys you want to add to the remote NASs authorised keys, so we&amp;rsquo;ll use ssh (with a password) to add them to the end of that file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh &amp;lt;user&amp;gt;@&amp;lt;remote NAS address&amp;gt; &amp;#39;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&amp;#39; &amp;lt; ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You need to substitute your remote NASs username and address, so mayby it would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh nas1_admin@83.78.2.105 &amp;#39;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&amp;#39; &amp;lt; ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When you execute this, it will ask for the remote password, but once it&amp;rsquo;s worked you should be able to ssh in and it allows that without using a password.&lt;/p&gt;
&lt;h4 id="tailscale-out"&gt;Tailscale out&lt;/h4&gt;
&lt;p&gt;Perhaps you didn&amp;rsquo;t get as far as needing the ssh password, because when you tried to ssh to the remote, ssh didn&amp;rsquo;t even recognise the domain. If you are using Tailscale to connect your devices (which I recommend) then there are two tricks needed.&lt;/p&gt;
&lt;p&gt;Trick one is to get around the fact that since DSM 7, Synology have prevented (for good security reasons) external packages from making outbound connections. So you&amp;rsquo;ll be able to use Tailscale to access the Synology web interface, or even ssh &lt;em&gt;into&lt;/em&gt; it, but you won&amp;rsquo;t be able to ssh &lt;em&gt;out&lt;/em&gt; of it. When I first discovered this, I was running &lt;code&gt;ip a&lt;/code&gt; at the command line in the local NAS and noticed that the tailscale IP was not even listed - it was as if Tailscale wasn&amp;rsquo;t running, but I knew it was since I had ssh&amp;rsquo;d in with the Tailscale address.&lt;/p&gt;
&lt;p&gt;Tailscale have a fix for &lt;a href="https://tailscale.com/kb/1131/synology#enable-outbound-connections"&gt;enabling outbound connections via Tailscale on Synology&lt;/a&gt;, you need to run a thing on reboot to enable the TUN.&lt;/p&gt;
&lt;p&gt;Trick two is that even after you&amp;rsquo;ve done that and rebooted and can see the tailscale interface when you run &lt;code&gt;ip a&lt;/code&gt;, you still won&amp;rsquo;t be able to use the Tailscale &amp;lsquo;magic&amp;rsquo; DNS but will have to use the Tailscale IP address for the remote when you ssh (and later rsync) to it. So I can&amp;rsquo;t use:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ssh nas1_admin@NAS-01&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;as I would normally from my laptop, I have to use &lt;code&gt;ssh nas1_admin@104.43.22.181&lt;/code&gt; If you are not sure of the Tailscale IP for your remote, have a look at your &lt;a href="https://login.tailscale.com/admin/machines"&gt;machines list&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="turn-rsync-on"&gt;Turn rsync on&lt;/h4&gt;
&lt;p&gt;Via the web interface on both Synologys, you&amp;rsquo;ll need to enable rsync. The setting is in &lt;code&gt;Control Panel | File Services | rsync&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-25-at-1.56.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-25-at-1.56.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Leave the port as 22 and don&amp;rsquo;t bother with the other settings, but do hit &lt;code&gt;Apply&lt;/code&gt; at the bottom to save the change.&lt;/p&gt;
&lt;h4 id="give-it-a-try"&gt;Give it a try&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;re now at the stage where you should be able to ssh into the remote NAS from the local one without being asked for a password, and rsync is turned on both ends, so in theory, you should be able to do something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m not going to go into all the flags for rsync (the internet has plenty of good guides for that) except to say that the &amp;rsquo;n&amp;rsquo; on the end of the flags in the command above means that no files will actually be moved, it will do a &amp;lsquo;dry run&amp;rsquo; and tell you what it would have done if you let it.&lt;/p&gt;
&lt;p&gt;Note that if you have a jazillion files, this could take a while, you might be better to limit it to a smaller sub directory such as &lt;code&gt;/volume1/media/music/napster/Metallica&lt;/code&gt;/&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/yo-dawg-heard-you.jpg" width="497" alt=""&gt;
&lt;p&gt;The other bit of free rsync advice I&amp;rsquo;ll give you is to look carefully at the source and destination directories in the command above. The source sub directory has a trailing &amp;lsquo;/&amp;rsquo;, the destination does not. If you mess this up you&amp;rsquo;ll be making directories inside your directories dawg.&lt;/p&gt;
&lt;p&gt;In theory once you&amp;rsquo;re at this point, everything should work. But here&amp;rsquo;s a couple of other bumps / thoughts.&lt;/p&gt;
&lt;h4 id="eadir"&gt;@eaDir&lt;/h4&gt;
&lt;p&gt;Synology has a bunch of hidden directories with metadata stuff. My advice is don&amp;rsquo;t mess with them, but also don&amp;rsquo;t sync them over either. Tell rsync to ignore them. Same for the recycle bin.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="permissions"&gt;Permissions&lt;/h3&gt;
&lt;p&gt;The user that you&amp;rsquo;re ssh&amp;rsquo;ing with needs to have permissions to all the places you are rsync&amp;rsquo;ing files to. Even though I&amp;rsquo;ve only ever had one user for each of my Synology NAS&amp;rsquo;s, and everything has been done by that one user either through the web GUI or command line, the files and directories on my NAS have a mixture of owners (my user and root) and permissions. Someone smarter than me could probably figure out why - and if your NAS has to include files from multiple users etc, you are going to need to do that. Because I like sledgehammers, all I did was ssh into the remote and:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo chown -R nas1_admin:users /volume1/media
sudo chmod -R 775 /volume1/media
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="throttling-bandwidth"&gt;Throttling bandwidth&lt;/h4&gt;
&lt;p&gt;If I saturate the downlink at my remote site while I&amp;rsquo;m rsync-ing a bunch of files, the users there will be unhappy when they can&amp;rsquo;t stream video reliability or if they&amp;rsquo;re getting killed in online games due to lag.&lt;/p&gt;
&lt;p&gt;rsync has a flag for that. If we want to limit the transfer bandwidth to 500KB it could look like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="get-destructive"&gt;Get destructive&lt;/h4&gt;
&lt;p&gt;If you only want to sync the files one way from your local to remote, then we can add a flag that will delete any files on the remote machine that are not present on the local one. Obviously use with care, and run with the -n flag first to see what&amp;rsquo;s going to get chopped.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1 --del
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="do-something-else"&gt;Do something else&lt;/h4&gt;
&lt;p&gt;The first few times you do this, it will be exciting watching the terminal window as rsync carefully checks for each file and directory and copies them over as needed, and it will also be helpful to see what errors might pop up so you can sort them out.&lt;/p&gt;
&lt;p&gt;Eventually though, it will be so routine and error free you&amp;rsquo;d rather do something else, so you&amp;rsquo;ll wander off and leave it, then curse when you return to find your laptop turned itself off due to inactivity and wrecked the rsync. Don&amp;rsquo;t panic, rsync is robust and will pick right up next time you run it without damaging any files, but you might also consider doing it all on remote control.&lt;/p&gt;
&lt;p&gt;On the local NAS, create a file called &lt;code&gt;sync-media.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash

nohup rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1 &amp;gt; sync_media.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;chmod +x sync_media.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you run it &lt;code&gt;./sync-media.sh&lt;/code&gt; you can log off and let it do it&amp;rsquo;s thing.&lt;/p&gt;</description></item><item><title>Containerised NGINX Proxy Manager &amp; the 502 error</title><link>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</link><pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png" width="695" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re used to running NGINX Proxy Manager in front of your web apps, and switch to running it in a container, you&amp;rsquo;re going to need to learn a little about Docker networks to get everything connected. If you just do your regular setup, and direct the proxy for an address to &lt;code&gt;127.0.0.1:&amp;lt;some port&amp;gt;&lt;/code&gt;, it won&amp;rsquo;t exist, and you&amp;rsquo;ll visit your page to find the &amp;ldquo;502 Bad Gateway openresty&amp;rdquo; message.&lt;/p&gt;
&lt;p&gt;If you pause to think for a second it will be obvious why - with NGINX Proxy Manager (I&amp;rsquo;m going to start calling it NPM to save myself some typing) &lt;em&gt;inside&lt;/em&gt; a container, any addresses you&amp;rsquo;re entering into the web interface when setting up proxys are &lt;em&gt;inside&lt;/em&gt; the NPM container. &lt;code&gt;127.0.0.1&lt;/code&gt; from that point of view refers to the inside of the NPM container, and not the host, so you exposing a port from your container to the host is not going to work.&lt;/p&gt;
&lt;p&gt;The fix for this is pretty simple, but first let&amp;rsquo;s look at the exception.&lt;/p&gt;
&lt;h3 id="the-exception"&gt;The Exception&lt;/h3&gt;
&lt;p&gt;Usually the very first proxy I add in NPM is to it&amp;rsquo;s own admin interface on port 81. Since this &lt;em&gt;is&lt;/em&gt; inside the NPM container, the setup looks exactly the same as if you were running on the host, and may well be why you were lulled into a false sense of familiarity.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a little bit confusing, since we can also manually access NPM from &lt;code&gt;http://127.0.0.1:81&lt;/code&gt; on the host if we&amp;rsquo;ve exposed port 81 in our compose file, but it&amp;rsquo;s actually a different route. In fact, we could hide port 81 from the host, and the setting above will still work.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my compose file, notice I haven&amp;rsquo;t exposed port 81&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 nginx-proxy-manager:
 image: &amp;#39;jc21/nginx-proxy-manager:latest&amp;#39;
 container_name: nginx-proxy-manager
 restart: unless-stopped
 ports:
 - &amp;#39;80:80&amp;#39;
 - &amp;#39;443:443&amp;#39;
 volumes:
 - ./data:/data
 - ./letsencrypt:/etc/letsencrypt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So if we try to access port 81 from the host, it won&amp;rsquo;t be answering.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But inside the container, 127.0.0.1 refers to itself, so if we open a shell into the container, the URL http://127.0.0.1:81 refers to something that exists, from there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="getting-out"&gt;Getting Out&lt;/h3&gt;
&lt;p&gt;So how can our NPM point to a service that&amp;rsquo;s running in another container? You are probably used to specifying ports: in your compose file to expose internal container ports to a host, but that doesn&amp;rsquo;t really help us since NPM in a container can not easily access the host&amp;rsquo;s ports. What we&amp;rsquo;d really like is for NPM to be able to access the second container&amp;rsquo;s ports. And in an ideal world, we&amp;rsquo;d like that to be the only way to access them - that way all the access to our second container service is forced through our proxy. Sounds like security no?&lt;/p&gt;
&lt;p&gt;Turns out this is simple thanks to the magic of Docker networks.&lt;/p&gt;
&lt;p&gt;You can get a fair way with Docker without really thinking or knowing about &lt;a href="https://docs.docker.com/engine/network/"&gt;Docker networks&lt;/a&gt;, and I&amp;rsquo;m really only covering the very basics here - you should probably invest some time in learning about this sometime. Meanwhile, lets run the &lt;code&gt;docker network ls&lt;/code&gt; command and see what networks we&amp;rsquo;ve got.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That network &lt;code&gt;nginx-proxy-manager_default&lt;/code&gt; is the one we&amp;rsquo;re interested in. Its name is just the container name with &amp;ldquo;_default&amp;rdquo; added on the end. What we need to do is just make sure the second container is included in that same network. That&amp;rsquo;s a matter of declaring the external network with that name, and including it in our service definition. I&amp;rsquo;m going to use an NGINX server in my example, but it could be anything. Here&amp;rsquo;s the compose for the second container.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 nginx-example.com:
 image: nginx
 container_name: nginx-example.com
 volumes:
 - ./www:/usr/share/nginx/html
 - ./conf/:/etc/nginx/conf.d/:ro
 restart: unless-stopped
 networks:
 - nginx-proxy-manager_default

networks:
 nginx-proxy-manager_default:
 external: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that I haven&amp;rsquo;t exposed any ports to the host here; We&amp;rsquo;re not going to need them since we&amp;rsquo;ll access this container direct from the NPM container via the internal Docker network docker created for us. That little network even contains a DNS server, so we don&amp;rsquo;t even need to worry about the container&amp;rsquo;s IP addresses, we can just use their names.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.52.54-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So any web requests to &amp;ldquo;example.com&amp;rdquo; arriving at our host go to NPM (since I&amp;rsquo;ve exposed ports 80 &amp;amp; 443 - see the top compose file). Then using the proxy I&amp;rsquo;ve added above, they are forwarded to the container named &amp;ldquo;nginx-example.com&amp;rdquo; which is a DNS record inside the Docker network that Docker created for us, and which both the NPM, and my service, containers are members of.&lt;/p&gt;</description></item><item><title>Moving from Docker volumes to bind mounts</title><link>https://blog.iankulin.com/moving-from-docker-volumes-to-bind-mounts/</link><pubDate>Mon, 05 Aug 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-from-docker-volumes-to-bind-mounts/</guid><description>&lt;p&gt;&lt;a href="https://placesjournal.org/article/all-is-lost-notes-on-broken-world-design/"&gt;&lt;img src="https://blog.iankulin.com/images/friedman-moe-lost-6.jpg" width="600" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I started with Docker, the docs seemed to suggest that using Docker volumes was a good thing. With a Docker volume, you just create the volume and Docker manages the rest. You don&amp;rsquo;t have to worry about where it is, or really ever think about it.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a docker-compose for &lt;a href="https://github.com/louislam/uptime-kuma/wiki"&gt;Uptime Kuma&lt;/a&gt; using a volume.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 uptime-kuma:
 image: louislam/uptime-kuma:1
 container_name: uptime-kuma
 volumes:
 - kuma_data:/app/data
 ports:
 - 80:3001
 restart: unless-stopped

volumes:
 kuma_data:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is telling Docker we want to create a volume called &amp;ldquo;kuma_data&amp;rdquo; and then map it into the container file system at &lt;code&gt;/app/data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t love this for a couple of reasons. The first is that I don&amp;rsquo;t know how to back that volume up - although this is a commonly quoted reason for using them. And the second is that, in order to make moving containers around easier, I have settled on a sort of standard setup. All Docker containers are in a subdirectory in my home directory where the subdirectory is their name. In that subdirectory is a docker-compose file to manage them, and a further subdirectory named &amp;lsquo;data&amp;rsquo; that holds all their data.&lt;/p&gt;
&lt;p&gt;For example, here&amp;rsquo;s my Jellyfin directory. The Jellyfin docker container needs two &amp;lsquo;volumes&amp;rsquo; - config and cache:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-2.57.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-2.57.34-pm.png" width="907" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So if I ever needed to move this somewhere, I could &lt;code&gt;docker compose down&lt;/code&gt;, then &lt;code&gt;rsync&lt;/code&gt; the whole directory somewhere, then &lt;code&gt;docker compose up&lt;/code&gt; &lt;code&gt;-d&lt;/code&gt;, and I&amp;rsquo;d be in business.&lt;/p&gt;
&lt;p&gt;Instead of using Docker volumes, I&amp;rsquo;m using &amp;lsquo;bind mounts&amp;rsquo; to those sub-directories. The docker-compose is not more complicated, if anything, it&amp;rsquo;s simpler since we don&amp;rsquo;t need the &lt;code&gt;volumes&lt;/code&gt; part at the bottom.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 jellyfin:
 image: jellyfin/jellyfin
 container_name: jellyfin
 network_mode: host
 ports:
 - &amp;#34;8096:8096&amp;#34;
 volumes:
 - ./data/config:/config
 - ./data/cache:/cache
 - /mnt/media:/media
 restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="pros-and-cons"&gt;Pros and Cons&lt;/h3&gt;
&lt;p&gt;Many people and projects prefer Docker volumes. My system of just using bind mounts might increase the likelihood of me breaking something by monkeying around with the files - and volumes reduce that possibility since they&amp;rsquo;d be hidden away somewhere and I guess owned by root. There are also some performance considerations if running under Mac or Windows since Docker Desktop in those cases is really running your container on a VM, and if you let it manage the volumes it can do so in the native file system.&lt;/p&gt;
&lt;p&gt;Another downside is that bind mount locations are not automatically initialised in the same way as volumes.&lt;/p&gt;
&lt;p&gt;Nevertheless, for my use, I like to use bind mounts and keep everything together.&lt;/p&gt;
&lt;h3 id="moving-advice"&gt;Moving advice&lt;/h3&gt;
&lt;p&gt;Since I started with volumes, I now needed to be able to change over to using bind mounts, and the first advice I googled up (such as &lt;a href="https://forums.docker.com/t/move-docker-volume-to-bind-mount/140843"&gt;here&lt;/a&gt; and &lt;a href="https://www.synoforum.com/threads/move-from-docker-volume-to-bind-mount.12990/"&gt;here&lt;/a&gt;) made it sound like it was going to be complicated. The advice was that the hidden file location of the volumes in the host system had some sort of magic about it and we needed to use a &lt;code&gt;docker cp&lt;/code&gt; command to access it or create a temporary container with the named volume and the bind mount you want to move it to, then copy the data across from inside that container.&lt;/p&gt;
&lt;h3 id="ignoring-advice"&gt;Ignoring advice&lt;/h3&gt;
&lt;p&gt;As long as the container is stopped when you access the files, I can&amp;rsquo;t see why we can&amp;rsquo;t just copy them as normal. Of course, I could be totally incorrect about this, but I&amp;rsquo;ve done it a number of times now, and not had any disasters. Of course, I always snapshot the VM before I start in case it doesn&amp;rsquo;t work, and you should as well. If you like to yolo your production data, here&amp;rsquo;s how.&lt;/p&gt;
&lt;h3 id="where-is-it"&gt;Where is it?&lt;/h3&gt;
&lt;p&gt;To find the actual location of the data in the host system, use the docker inspect command on the running container. You&amp;rsquo;ll get a bunch of JSON. About a third up from the bottom of that there&amp;rsquo;ll be a section called &amp;ldquo;Mounts&amp;rdquo; that will have the real location for each volume. (if you&amp;rsquo;re looking at a &amp;ldquo;Mounts&amp;rdquo; section that doesn&amp;rsquo;t have the &amp;ldquo;Source&amp;rdquo; line, scroll down a bit).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-3.27.33-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-21-at-3.27.33-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="copying"&gt;Copying&lt;/h3&gt;
&lt;p&gt;Next thing, I&amp;rsquo;ll make a data sub-directory for it. &lt;code&gt;mkdir ~/uptimekuma/data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The (with the container stopped), copy the data over.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo cp -a /var/lib/docker/volumes/uptimekuma_kuma_data/_data/. ~/uptimekuma/data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After checking that&amp;rsquo;s actually worked, I&amp;rsquo;ll kill the volume. We need to get the volume name first if you haven&amp;rsquo;t figured it out from the location path.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@ct390-test:~/uptimekuma$ sudo docker volume ls
DRIVER VOLUME NAME
local uptimekuma_kuma_data
ian@ct390-test:~/uptimekuma$ sudo docker volume rm uptimekuma_kuma_data 
uptimekuma_kuma_data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we&amp;rsquo;ll need to update the &lt;code&gt;docker-compose.yaml&lt;/code&gt; file from the top of this post so it bind mounts the sub-directory that we&amp;rsquo;ve copied to instead of the named volume.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services:
 uptime-kuma:
 image: louislam/uptime-kuma:1
 container_name: uptime-kuma
 volumes:
 - ./data:/app/data
 ports:
 - 80:3001
 restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that&amp;rsquo;s done, &lt;code&gt;docker compose up -d&lt;/code&gt; should get things running. And now we have a nice situation with the docker compose and all the data for the container together in one spot.&lt;/p&gt;</description></item><item><title>dockerfile - CMD vs ENTRYPOINT</title><link>https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/</link><pubDate>Mon, 22 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/</guid><description>&lt;p&gt;There are two entries we often have at the end of a &lt;code&gt;dockerfile&lt;/code&gt; (which is the file that tells Docker how an image is to be built).&lt;/p&gt;
&lt;p&gt;They are similar in that when the container is launched from an image, these commands will be executed. For example, both of the dockerfiles below will print &amp;ldquo;Hello World&amp;rdquo; when run.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doc-&lt;/code&gt;entry:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM debian:stable-slim
ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;doc-cmd&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM debian:stable-slim
CMD [&amp;#34;echo&amp;#34;, &amp;#34;Hello World&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.45.26-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The key difference between them is that CMD can be overridden:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.47.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see from this that the ENTRYPOINT command is just added on to by the extra command line argument, but the CMD one is replaced entirely.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have an ENTRYPOINT and a CMD in your dockerfile:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doc-both&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM debian:stable-slim
ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
CMD [&amp;#34;&amp;amp; Hello World from CMD&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.55.45-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Naturally, only the CMD is overridden if we pass in extra values.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.58.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h4 id="other-things-of-note"&gt;Other things of note&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Although these demos are at the command line, we&amp;rsquo;d see the same behaviour if we&amp;rsquo;d added a CMD to a docker compose file and started the container that way.&lt;/li&gt;
&lt;li&gt;You can have multiple ENTRYPOINTs or CMDs in a file, they are all ignored except the last one.&lt;/li&gt;
&lt;li&gt;The best place to learn more about &lt;a href="https://docs.docker.com/reference/dockerfile/#entrypoint"&gt;ENTRYPOINT&lt;/a&gt; and &lt;a href="https://docs.docker.com/reference/dockerfile/#cmd"&gt;CMD&lt;/a&gt; is in the official &lt;a href="https://docs.docker.com/reference/dockerfile/"&gt;Docker docs for dockerfile&lt;/a&gt;, not from an AI.&lt;/li&gt;
&lt;li&gt;Most times, what you&amp;rsquo;re looking at the end of your dockerfile is ENTRYPOINT. Just use CMD to add a default behaviour that you&amp;rsquo;re happy for your image users to overide.&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t confuse either of these with RUN - that happens during the image build, ENTRYPOINT and CMD are used when the container is launched/run.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>User environment variables are not available in cron</title><link>https://blog.iankulin.com/user-environment-variables-are-not-available-in-cron/</link><pubDate>Mon, 15 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/user-environment-variables-are-not-available-in-cron/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-4.13.13-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m used to using the &lt;code&gt;docker-compose.yaml&lt;/code&gt; or &lt;code&gt;dockerfile&lt;/code&gt; to set environment variables for containers running my apps, but ran into an issue recently where the variable seemed to be set some of the time, but at others it didn&amp;rsquo;t appear to exist.&lt;/p&gt;
&lt;p&gt;I had a script set to run by &lt;code&gt;cron&lt;/code&gt; inside the container, and it turns out that the environment variables set for the container are available in the user space, but not in &lt;code&gt;cron&lt;/code&gt;, even if running with that user&amp;rsquo;s permissions. This is probably old news to established Linux users but it threw me for a while. I&amp;rsquo;d &lt;code&gt;exec&lt;/code&gt; into the container and the script would work perfectly, then wait another minute for &lt;code&gt;cron&lt;/code&gt; to run it and it would fail 🤦‍♀️ It was exasperated by my discovery that I didn&amp;rsquo;t know how to console.log debug from inside a container cron job as well - the subject of an earlier post.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d narrowed it down to this issue and googleconfirmed it, I didn&amp;rsquo;t really come up with an elegant solution either. You may think &amp;ldquo;buy hey, everything in Linux is a file, it must be in proc somewhere&amp;rdquo;, and you&amp;rsquo;d be right, sort of.&lt;/p&gt;
&lt;p&gt;In Linux, to see your own environment variables, you type in &lt;code&gt;env&lt;/code&gt;. I think this probably works by grabbing them from &lt;code&gt;proc&lt;/code&gt; under &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/environ&lt;/code&gt; where &lt;PID&gt; is the process id of the shell. We can get our own process id with the &lt;code&gt;ps&lt;/code&gt; command. If we&amp;rsquo;re the root user (which I am in my container) we can see &lt;em&gt;someone else&amp;rsquo;s&lt;/em&gt; process ids with &lt;code&gt;ps -u &amp;lt;username&amp;gt;&lt;/code&gt;, get the process id for the shell and look in proc. So I tried that from the cron script - it turns out no.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-6.51.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I could see them, but it didn&amp;rsquo;t include the environment variable passed in from Docker that was available at the entry point. This is all a bit weird to me - I&amp;rsquo;m not sure why we&amp;rsquo;re the same user, with the same permissions but with a new seperate environment. I guess there is a reason, it&amp;rsquo;s just not apparent to me.&lt;/p&gt;
&lt;h3 id="the-hack"&gt;The Hack&lt;/h3&gt;
&lt;p&gt;To get around this, I now save the environment variable I&amp;rsquo;m interested in to a file in the script that runs at the entry point:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Save the environment variable IMAGE_URL into
# a file for later use in the cron script

env | grep IMAGE_URL &amp;gt; image_url.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then in my script that is run by cron, I reconstitute it from the file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Read the file saved by the entry point script
# and extract the environment variable

while IFS=&amp;#39;=&amp;#39; read -r key value; do
 if [[ $key == &amp;#34;IMAGE_URL&amp;#34; ]]; then
 export &amp;#34;$key=$value&amp;#34;
 fi
done &amp;lt; &amp;#34;/image_url.txt&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That works fine. Software testers will be looking at this solution thinking &amp;ldquo;What about the case where the environment variable isn&amp;rsquo;t set, but the file from the last run is still there?&amp;rdquo; Worry not, bug finding person. It&amp;rsquo;s a container so everything&amp;rsquo;s ephemeral. The file with the environment variable can only be there on runs when that environment variable has been set.&lt;/p&gt;</description></item><item><title>SSH login notification</title><link>https://blog.iankulin.com/ssh-login-notification/</link><pubDate>Mon, 13 May 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-login-notification/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/brown-bell-on-white-concrete-wall-4VRzuA4UxSY?utm_content=creditShareLink&amp;utm_medium=referral&amp;utm_source=unsplash"&gt;&lt;img src="https://blog.iankulin.com/images/nick-fewings-4vrzua4uxsy-unsplash.jpg" width="400" alt="Photo by Nick Fewings Unsplash
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My VPS&amp;rsquo;s are usually locked down so just ports 80 &amp;amp; 443 (for web server) and 22 (for ssh) are open. That&amp;rsquo;s great for reducing the attack surface, but having ssh open is a potentially disastrous vulnerability. For this reason I often close that at the cloud firewall level as well, but it has to be open when I&amp;rsquo;m making changes or running the weekly ansible update/cleanup playbooks.&lt;/p&gt;
&lt;p&gt;To make things a bit safer, I run &lt;a href="https://blog.iankulin.com/beginning-node-app-security/"&gt;Fail2Ban&lt;/a&gt; on the ssh logs, and also have notifications turned on via &lt;a href="https://ntfy.sh/"&gt;Ntfy&lt;/a&gt;. Ntfy is so useful I make an annual donation to support it&amp;rsquo;s development and help with Phil&amp;rsquo;s server costs. I recommend you do to. In fact, my setup for getting a notification on my watch everytime someone ssh&amp;rsquo;s into one of my VPS&amp;rsquo;s is just copied directly from &lt;a href="https://docs.ntfy.sh/examples/#__tabbed_1_1"&gt;Phil&amp;rsquo;s examples&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="changes"&gt;Changes&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re not logged in as root (that should be turned off) you&amp;rsquo;ll need to run all these as sudo.&lt;/p&gt;
&lt;p&gt;Edit &lt;code&gt;/etc/pam.d/sshd&lt;/code&gt; to add these lines to the bottom:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# at the end of the file
session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then create the file &lt;code&gt;/usr/bin/ntfy-ssh-login.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash
if [ &amp;#34;${PAM_TYPE}&amp;#34; = &amp;#34;open_session&amp;#34; ]; then
 curl \
 -H prio:high \
 -H tags:warning \
 -d &amp;#34;SSH login: ${PAM_USER} from ${PAM_RHOST}&amp;#34; \
 ntfy.sh/your-unique-notification-string
fi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;but replace &lt;code&gt;your-unique-notification-string&lt;/code&gt; with whatever string you&amp;rsquo;re monitoring with the app. Note that if you&amp;rsquo;re using the shared (rather than self hosted) service, these are public. If I&amp;rsquo;d used mine here, you&amp;rsquo;d be able to use it to spam my phone with alerts. For this reason, many people use GUIDs.&lt;/p&gt;
&lt;p&gt;We need to make this executable:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;chmod +x /usr/bin/ntfy-ssh-login.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And restart the ssh daemon&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then log out, and ssh back in, and you should get the notification.&lt;/p&gt;</description></item><item><title>Upgrading to Forgejo 7.0.1</title><link>https://blog.iankulin.com/upgrading-to-forgejo-7-0-1/</link><pubDate>Mon, 06 May 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/upgrading-to-forgejo-7-0-1/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-28-at-1.08.21-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-28-at-1.08.21-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not that long ago that &lt;a href="https://blog.iankulin.com/my-web-app-update-process/"&gt;I wrote about&lt;/a&gt; doing routine upgrades on containerised web apps using Forgejo as an example as I upgraded Forgejo (my git repository manager) between patch versions of 1.21, then a few days later, they dropped 7.0.0&lt;/p&gt;
&lt;p&gt;&lt;a href="https://forgejo.org/2024-04-release-v7-0/"&gt;They say&lt;/a&gt; the major version jump is due to it being an LTS (long term support) release, and changing to &lt;a href="https://semver.org/spec/v2.0.0.html"&gt;semantic versioning 2.0.0&lt;/a&gt; , but that doesn&amp;rsquo;t quite explain it to me, and I assume this is partly signifying the fork&amp;rsquo;s drift away from the gitea codebase. In any case, the upgrade to 7.0.0 it does involve some breaking changes, and signifies to me that a lot has been on, which makes me keen to wait for a patch release (I&amp;rsquo;m always keen for other people to debug these things) which has now landed.&lt;/p&gt;
&lt;p&gt;The reason I think the upgrade process is worth mentioning, is that the steps we went through to move from 1.21.0 to 1.21.8:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pull codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose up&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;will not work this time, and gives me the excuse to talk about container tags.&lt;/p&gt;
&lt;h3 id="container-tags"&gt;Container Tags&lt;/h3&gt;
&lt;p&gt;When the developers had built their release for 1.21.8, they would have pushed the exact same image to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21.8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;that way, people like me who had specified &lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt; in their docker-compose.yml files just had to down/pull/up to be in business.&lt;/p&gt;
&lt;p&gt;If they had released another patch version, say 1.21.10, they they would have pushed the new image to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeberg.org/forgejo/forgejo:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;i.e. the old 1.21.8 image would have stayed the same, so anyone who had depended on that not changing will still be fine, but people like me who want all the patch versions updated (but not a minor version change) would get the new one.&lt;/p&gt;
&lt;p&gt;Normally you can just click on &amp;rsquo;tags&amp;rsquo; for an image on Docker Hub, but since this one is hosted on Codeburg&amp;rsquo;s Forgejo instance, you need to go &lt;a href="https://codeberg.org/forgejo/-/packages/container/forgejo/versions"&gt;https://codeberg.org/forgejo/-/packages/container/forgejo/versions&lt;/a&gt; to see all the tags they&amp;rsquo;ve pushed to.&lt;/p&gt;
&lt;h3 id="upgrade-steps"&gt;Upgrade steps&lt;/h3&gt;
&lt;p&gt;The extra step we&amp;rsquo;ll need to go through this time is to decide what level of version we want to specify in our docker-compose. I&amp;rsquo;ll stick to specifying to the minor version so my new &lt;code&gt;docker-compose.yml&lt;/code&gt; will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;

networks:
 forgejo:
 external: false

services:
 server:
 image: codeberg.org/forgejo/forgejo:7.0
 container_name: forgejo
 environment:
 - USER_UID=112
 - USER_GID=103
 restart: always
 networks:
 - forgejo
 volumes:
 - ./forgejo:/data
 - /etc/timezone:/etc/timezone:ro
 - /etc/localtime:/etc/localtime:ro
 ports:
 - &amp;#39;80:3000&amp;#39;
 - &amp;#39;2200:22&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once that decision is made, it&amp;rsquo;s just the same:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;backup the LXC&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker pull codeberg.org/forgejo/forgejo:7.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker compose up&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Then some testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We could probably skip that pull step - when you compose up the system would notice the version change and pull it for us.&lt;/p&gt;</description></item><item><title>Peek inside a Docker image</title><link>https://blog.iankulin.com/peek-inside-a-docker-image/</link><pubDate>Mon, 29 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/peek-inside-a-docker-image/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.20.28-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.20.28-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A &amp;lsquo;dockerfile&amp;rsquo; contains all the instructions to build a Docker image. Here&amp;rsquo;s my first draft for a project I&amp;rsquo;m working on:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM node:20
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [&amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;COPY . .&lt;/code&gt; is copying all of the files in my project into the working directory of the image so they can be run. Of course we don&amp;rsquo;t need them all for the app - for example the &lt;code&gt;node_modules&lt;/code&gt; directory will be created when we &lt;code&gt;npm install&lt;/code&gt; so no need to copy that, and I don&amp;rsquo;t need all my dot files in the container.&lt;/p&gt;
&lt;p&gt;Docker has an easy fix for this, we can just add these files to a &lt;code&gt;.dockerignore&lt;/code&gt; file in the project, again, here a first draft.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;data
db
node_modules
.vscode
.dockerignore
.gitignore
.env
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I build an image, it doesn&amp;rsquo;t list the files it&amp;rsquo;s copying in, so I often like to sneak inside the image to have a look. This is easy, the trick is just to launch bash inside there. When I built this particular image, I tagged it &lt;code&gt;iankulin/tick&lt;/code&gt;, so the command to run bash inside it is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run -it iankulin/tick /bin/bash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those flags, &lt;code&gt;-it&lt;/code&gt; are saying we want an interactive terminal. To get back out of it, just use &lt;code&gt;ctrl-D&lt;/code&gt; the sames as if you where logging out of an ssh session.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.27.22-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.27.22-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well well, there are a few files there I can add to the &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a couple of reasons to only keep necessary files in our containers. The first is that it just seems like good programming craft to keep things neat and clean, and a second is that it could become a security issue if we leak things into our containers. An obvious one would be a .&lt;code&gt;env&lt;/code&gt; that contained API keys or similarly sensitive stuff, but also, I have no idea what&amp;rsquo;s in a &lt;code&gt;.DS_Store&lt;/code&gt;. Mostly likely nothing important, but it&amp;rsquo;s not needed by my app so lets eliminate it by adding it to &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You might think I could have avoided all this by explicitly copying the files I know I need in the &lt;code&gt;dockerfile&lt;/code&gt; instead of using the broadbrush &lt;code&gt;COPY . .&lt;/code&gt; and that&amp;rsquo;s true. But I&amp;rsquo;ve found that if I do that, I end up wasting time debugging things that turn out to be a missing file, whereas if I copy everything, I just need to inspect the container at the start of the project and again as part of the shipping checks and we&amp;rsquo;re golden.&lt;/p&gt;
&lt;p&gt;Actually, I generally don&amp;rsquo;t want any dot files in my containers, so we&amp;rsquo;ll add that as a wildcard in the .dockerignore&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;data
db
node_modules
.*
dockerfile
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Much neater:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.42.56-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-25-at-10.42.56-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>NGINX Proxy Manager</title><link>https://blog.iankulin.com/nginx-proxy-manager/</link><pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-proxy-manager/</guid><description>&lt;p&gt;I&amp;rsquo;ve mentioned using NGINX as an &lt;a href="https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/"&gt;interface between the internet and a service&lt;/a&gt; a while ago. This works by all incoming traffic coming to NGINX, and NGINX determining which service that traffic should go (from the NGINX config files) then acting as a middleman. This functionality is generally referred to as a &amp;lsquo;reverse proxy&amp;rsquo;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/nginx.png" width="959" alt="Terrible drawing of NGINX proxying requests off to different services."&gt;
&lt;p&gt;This is nice for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can have a single point of entry to the services, easier to lock down and secure, with access centrally logged&lt;/li&gt;
&lt;li&gt;The services can be running on all sorts of odd addresses and ports (for example 192.168.101.23:4002) but they can be addressed with sensible names by the user (such as todo.example.com)&lt;/li&gt;
&lt;li&gt;We can add &lt;a href="https://blog.iankulin.com/quick-dirty-auth-with-nginx-node/"&gt;basic auth&lt;/a&gt; to any services that need it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All this stuff is managed through the &lt;a href="https://blog.iankulin.com/nginx-config-on-debian-ubuntu/"&gt;NGINX config&lt;/a&gt; files. Perhaps one might look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
 listen 80;
 server_name example.com;

 location / {
 root /var/www;
 index index.html;
 }

 location /app2/ {
 proxy_pass http://192.168.101.65:8096/;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 }

 location /secure_area/ {
 auth_basic &amp;#34;Restricted&amp;#34;;
 auth_basic_user_file /etc/nginx/.htpasswd;
 }
}

server {
 listen 80;
 server_name app1.example.com;

 location / {
 proxy_pass http://192.168.101.23:4000;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These config files are powerful, and in the usual trade-off somewhat complicated and I&amp;rsquo;ve certainly made problems for myself in the past by making errors in them.&lt;/p&gt;
&lt;p&gt;There is a great project, &lt;a href="https://nginxproxymanager.com/"&gt;NGINX Proxy Manager&lt;/a&gt; that throws a nice web GUI on this process. On top of that, it makes the process of obtaining &lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; SSL certificates even easier than CertBot.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/npm.png" width="946" alt="Terrible drawing of NGINX Proxy Manager proxying requests off to different service, and obtaining SSL certificates for them."&gt;
&lt;p&gt;NGINX Proxy Manager is available as a docker image, and is trivial to set up if you&amp;rsquo;re used to docker. Once that&amp;rsquo;s done, the process of adding the proxies is simple enough that you probably don&amp;rsquo;t need any instructions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-31-at-8.59.56-am.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="alternatives"&gt;Alternatives&lt;/h3&gt;
&lt;p&gt;Rolling your own, or using NGINX Proxy Manager are not your only options. There&amp;rsquo;s also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.haproxy.org/#desc"&gt;HAProxy&lt;/a&gt; - an industrial strength proxy/load balancer&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/quick-starts/reverse-proxy"&gt;Caddy&lt;/a&gt; - same as NGINX but different. Has a great plugin architecture. A particular plugin &lt;a href="https://github.com/lucaslorentz/caddy-docker-proxy"&gt;Caddy-docker-proxy&lt;/a&gt; enables configuration of each service with tables inside the service&amp;rsquo;s docker-compose file which is a particularly neat trick.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://traefik.io/traefik/"&gt;Traefik&lt;/a&gt; - does a similar trick to Caddy-docker-proxy of figuring out it&amp;rsquo;s config from the services it&amp;rsquo;s proxying. It&amp;rsquo;s a serious bit of kit valuable for putting in front of huge Kubernetes swams, and is therefore probably a bit more complex to manage than Caddy-docker-proxy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I haven&amp;rsquo;t used any of these (except NGINX Proxy Manager) so take these descriptions as a starting point only.&lt;/p&gt;
&lt;p&gt;For any self-hosted (at home or on a VPS) services, you are going to need some of this functionality, and NGINX Proxy Manager is a simple, robust approach that should definitely be considered.&lt;/p&gt;</description></item><item><title>My Web App Update Process</title><link>https://blog.iankulin.com/my-web-app-update-process/</link><pubDate>Mon, 01 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/my-web-app-update-process/</guid><description>&lt;p&gt;I&amp;rsquo;ve settled on a very standard, reproducible setup for services in my homelab. This post looks at that, then runs through the update I did today to Forgejo which only took a few minutes and felt relatively risk free.&lt;/p&gt;
&lt;h3 id="standard-setups"&gt;Standard Setups&lt;/h3&gt;
&lt;p&gt;My system is based around Proxmox. I have three physical machines - one for production apps, a production spare, and a development/testbed machine. A Synology NAS serves for backups. Moving a VM or LXC between the machines is trivial; but it&amp;rsquo;s done manually - the machines are not clustered for high availability.&lt;/p&gt;
&lt;p&gt;Most workloads are Docker containers &lt;em&gt;inside&lt;/em&gt; an LXC. This works fine with a couple of caveats. I have an LXC template saved with Docker and Tailscale installed, my non-root user added, the mount for the NAS, and SSH keys. Setting up a new app starts with a full clone of this, a &lt;code&gt;dpkg-reconfigure openssh-server&lt;/code&gt; and &lt;code&gt;tailscale up&lt;/code&gt; and changing the root &amp;amp; non-root users&amp;rsquo; passwords.&lt;/p&gt;
&lt;p&gt;Next I create a sub directory for the app and write the &lt;code&gt;docker-compose.yaml&lt;/code&gt; in there. Then it&amp;rsquo;s just a matter of &lt;code&gt;docker compose up -d&lt;/code&gt;. If there&amp;rsquo;s any data, it goes in a another sub directory off this one.&lt;/p&gt;
&lt;p&gt;Unless I need something else, nightly backups to the NAS happen automatically for all the VMs and containers handled by a setting in Proxmox.&lt;/p&gt;
&lt;h3 id="upgrading-an-app"&gt;Upgrading an App&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve noticed a couple of posts about a new release of &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; on Mastodon in the past few days, so I figure I should look at that. My version is 1.21.1 and the new one is 1.21.8&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.44.36-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Because of &lt;a href="https://semver.org/"&gt;semantic versioning&lt;/a&gt;, I&amp;rsquo;m confident this is not going to break anything, but I check the release notes anyway. It looks good.&lt;/p&gt;
&lt;h4 id="backup"&gt;Backup&lt;/h4&gt;
&lt;p&gt;I jump into the Proxmox web gui and make a backup of the container.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.47.06-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;h4 id="docker-compose"&gt;Docker Compose&lt;/h4&gt;
&lt;p&gt;I ssh in to look at the image tag in the docker-compose.yml file. The reason I&amp;rsquo;m interested in this is that if the compose is set to &lt;code&gt;codeberg.org/forgejo/forgejo:1.21.1&lt;/code&gt; then it will be locked into that patch version, but it says &lt;code&gt;codeberg.org/forgejo/forgejo:1.21&lt;/code&gt; so we&amp;rsquo;re good.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.48.38-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now I take the service down from the CLI with &lt;code&gt;sudo docker compose down&lt;/code&gt;, then pull the new image with &lt;code&gt;sudo docker pull codeberg.org/forgejo/forgejo:1.21&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-9.24.21-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The to start it again, it&amp;rsquo;s just a &lt;code&gt;docker compose up -d&lt;/code&gt; and we&amp;rsquo;re live again.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.52.45-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-24-at-8.52.45-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="testing"&gt;Testing&lt;/h4&gt;
&lt;p&gt;My testing of this was pretty brief since (a) I&amp;rsquo;ve got high confidence in the developers at &lt;a href="https://blog.iankulin.com/gogs-gitea-forgejo/"&gt;gitea and forgejo&lt;/a&gt; and (b) this app gets pretty much daily use so if there are issues I&amp;rsquo;ll surface them pretty quickly, (c) anything I&amp;rsquo;m actively working on had full git histories on my laptop, and (d) the releases since my last update are pretty much just bug fixes.&lt;/p&gt;
&lt;p&gt;Nevertheless, I clicked around the web gui, and tried some pushes, pulls and clones and everything seemed fine.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m very comfortable with the way I&amp;rsquo;ve put all this together now. It&amp;rsquo;s a reliable, easily managed setup that makes maintenance like this simple and safe.&lt;/p&gt;</description></item><item><title>Deploying a Node app in Docker</title><link>https://blog.iankulin.com/deploying-a-node-app-in-docker/</link><pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/deploying-a-node-app-in-docker/</guid><description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Cargo_ship#/media/File:Cargo_Ship_Puerto_Cortes.jpg"&gt;&lt;img src="https://blog.iankulin.com/images/cargo_ship_puerto_cortes.jpg" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I wrote the install instructions for mdserver (little Markdown server Node app) on it&amp;rsquo;s &lt;a href="https://github.com/IanKulin/mdserver"&gt;github page&lt;/a&gt; it was something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have node.js installed and working&lt;/li&gt;
&lt;li&gt;Clone the repo&lt;/li&gt;
&lt;li&gt;Start with &lt;code&gt;npm start&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is great if you know &lt;a href="https://blog.iankulin.com/installing-a-node-app-on-a-server/"&gt;how to do those things&lt;/a&gt; (they are bread and butter to a web dev) but not if you&amp;rsquo;re a self-hoster who just wants a web server that converts markdown to HTML on the fly. For any situation where you just want to use the app, what you probably want is a Docker image of the app.&lt;/p&gt;
&lt;h3 id="docker"&gt;Docker&lt;/h3&gt;
&lt;p&gt;Docker &lt;em&gt;containers&lt;/em&gt; are similar to a virtual machine in the sense that they need to be hosted, and are relatively isolated from other processes except is some explicitly defined ways. Docker images are stored in repositories (the default one is &lt;a href="https://hub.docker.com/"&gt;DockerHub&lt;/a&gt;). It probably sounds like a wasteful process to ship an entire operating system with every little app - this is somewhat overcome by the images being built up in layers, and duplicated layers don&amp;rsquo;t need to be shlipped around since they are cached.&lt;/p&gt;
&lt;p&gt;So to deploy our Node app as a Docker container, we need to build an image, and store it on Docker Hub. From there, users can deploy it from their command lines by calling it directly or declaratively with a &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;h3 id="dockerfile"&gt;Dockerfile&lt;/h3&gt;
&lt;p&gt;To create a Docker image of our app that can be distributed, we run the &lt;code&gt;docker build&lt;/code&gt; command which reads a file named &lt;code&gt;Dockerfile&lt;/code&gt; to create the image. Here&amp;rsquo;s the &lt;code&gt;Dockerfile&lt;/code&gt; for the mdserver app.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Use an official Node.js runtime as the base image
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json to the working directory
COPY package*.json .

RUN npm install

# Copy the rest of the application source code to the container
COPY ./server.js .
COPY ./LICENSE .
COPY ./readme.md .

# Expose the port that the Node.js app will listen on
EXPOSE 3000

# Define the command to start your Node.js app
CMD [ &amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34; ]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FROM&lt;/code&gt; keyword specifies the base image we&amp;rsquo;re starting with. It could just be something like a Debian base, then in the following commands in the Dockerfile we&amp;rsquo;d install Node, but Node (and lots of other web dev tool builders) have provided &lt;a href="https://hub.docker.com/_/node/"&gt;official Docker images&lt;/a&gt; that they have crafted to make it easier for us. In this case I&amp;rsquo;m specifying that I want the image based on the lightweight Alpine Linux distro, with version 20 of Node installed on it.&lt;/p&gt;
&lt;p&gt;Note that when Node created the &lt;code&gt;node:20-alpine&lt;/code&gt; image, their Dockerfile probably started with &lt;code&gt;FROM [alpine:3.18](https://hub.docker.com/_/alpine)&lt;/code&gt; - you see? Layers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WORKDIR /usr/src/app&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So now we&amp;rsquo;ve got a container with a fully working install of Linux (or close enough to that so we can think of it like that - I&amp;rsquo;m pretty sure there&amp;rsquo;s no kernel). This command is saying that all the next commands are going to refer to the working directory &lt;em&gt;inside&lt;/em&gt; the container as &lt;code&gt;/usr/src/app&lt;/code&gt;. In effect its as if you&amp;rsquo;d ssh&amp;rsquo;d in and run &lt;code&gt;mkdir /usr/scr/app &amp;amp;&amp;amp; cd mkdir /usr/scr/app&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY package*.json .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve written before about the &lt;a href="https://blog.iankulin.com/sorting-out-node-package-dependencies-when-cloning-old-repos/"&gt;intricacies of the package files in Node&lt;/a&gt;. Basically these files (&lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;) specify the dependencies for out project. The dependencies are all sitting in the node_modules folder, but having a listing of them in the package files means we can just check them into source control and not worry about that bloated folder.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;COPY&lt;/code&gt; command, just copies them both into out container image - the &lt;code&gt;.&lt;/code&gt; at the end just means the current working directory inside the container - ie &lt;code&gt;/usr/src/app&lt;/code&gt; in our case.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RUN npm install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now the package files are inside the container, we just run &lt;code&gt;npm install&lt;/code&gt;, exactly the same as we would on a server, in order to download all of the dependencies for our app into the container. If that looks like you could just say &lt;code&gt;RUN&lt;/code&gt; then run any old Linux command then you&amp;rsquo;re getting the hang of it. You can &lt;code&gt;apt install&lt;/code&gt; stuff, &lt;code&gt;echo&lt;/code&gt; a line into a config file - whatever you need.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./server.js .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./LICENSE .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./readme.md .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For this trivial app, we only need the one source file, but I like to copy the license and readme in as well. It&amp;rsquo;s possible for future users of the container to run commands in their copy of the container, so it&amp;rsquo;s conceivable someone might look in here to read them. Once again, the second parameter specifies where in the container the files are copied to, and once again we&amp;rsquo;ve said the current work dir.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll very commonly see &lt;code&gt;COPY . .&lt;/code&gt; in Dockerfiles. This is saying copy all the files in the current directory to the working directory inside the container image. I guess that way you don&amp;rsquo;t miss anything, but do I really need a copy of my &lt;code&gt;Dockerfile&lt;/code&gt;, my vscode settings, my &lt;code&gt;node_modules&lt;/code&gt; folder in the image? No. There is a way to avoid copying that stuff in - add a &lt;code&gt;.dockerignore&lt;/code&gt; file to your project. This works exactly like a &lt;code&gt;.gitignore&lt;/code&gt; - you just list one file or directory per line, and then the &lt;code&gt;COPY&lt;/code&gt; command will know not to bother with it,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My node app is set to use port 3000, so we need to tell Docker to open that port for us since by default everything&amp;rsquo;s locked down. Note that the user of this container won&amp;rsquo;t be stuck with this decision, when they start the container, they can specify where in the outside world this internal container is going to be mapped to. That could be port &lt;code&gt;8080&lt;/code&gt;, &lt;code&gt;80&lt;/code&gt; or whatever.&lt;/p&gt;
&lt;p&gt;CMD [ &amp;ldquo;node&amp;rdquo;, &amp;ldquo;server.js&amp;rdquo; ]&lt;/p&gt;
&lt;p&gt;Finally, Docker needs to know how to start our app. This command is not being run now (when we&amp;rsquo;re building the image) it&amp;rsquo;s used by Docker when it launches the containerised app. I&amp;rsquo;m not sure why it is an array of strings instead of just a string, but it is. Just break it at each space in your command to run the app.&lt;/p&gt;
&lt;p&gt;If you look back at the list of manual steps I started this post with, you&amp;rsquo;ll see that we&amp;rsquo;ve pretty much just re-implemented them in the Dockerfile:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set up a node environment&lt;/li&gt;
&lt;li&gt;copy the files in&lt;/li&gt;
&lt;li&gt;run server.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously there&amp;rsquo;s lots more you can do with Dockerfiles, but the underlying concept is pretty straightforward - you&amp;rsquo;re setting up the whole environment for your app to run in so it can be mostly independent from its host OS.&lt;/p&gt;
&lt;h3 id="build-step"&gt;Build Step&lt;/h3&gt;
&lt;p&gt;To create the image from the Dockerfile, you are going to need Docker. I&amp;rsquo;m working on a Mac so I&amp;rsquo;ve got &lt;a href="https://www.docker.com/products/docker-desktop/"&gt;Docker Desktop&lt;/a&gt; installed. When it&amp;rsquo;s running there&amp;rsquo;s the little whale up in the toolbar.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t need a &lt;a href="https://hub.docker.com/"&gt;DockerHub&lt;/a&gt; account to build the image, but you&amp;rsquo;ll need one to upload it, and for naming your build, so head there now and create one. It is possible to use other registries for storing your images, but by default docker looks at it&amp;rsquo;s own registry, so that&amp;rsquo;s the best place to start when you&amp;rsquo;re figuring things out.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re working with Docker images and registries, to uniquely identify an image, it usually has a name format like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;username&amp;gt;/&amp;lt;imagename&amp;gt;:&amp;lt;tag&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Usually the tag will be a version number, or perhaps &lt;code&gt;:latest&lt;/code&gt;. The build command for our image could be this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build -t iankulin/mdserver:latest&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.27.51-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This will load the .dockerignore then step through the Dockerfile to build our image. The image is stored away by Docker - we don&amp;rsquo;t need to worry about where. You can get the list at the command line with &lt;code&gt;docker images&lt;/code&gt;, or if you&amp;rsquo;re running Docker Desktop, on the &amp;lsquo;images&amp;rsquo; tab.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.36.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.36.22-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have skipped quite a bit of detail about the build step and options. For example I sometimes use the &lt;code&gt;--platform&lt;/code&gt; flag to specify &lt;code&gt;linux/amd64&lt;/code&gt; if I&amp;rsquo;m testing on one of my homelab VMs rather than &lt;code&gt;linux/arm64&lt;/code&gt; if I&amp;rsquo;m running the container on the mac. Also, we don&amp;rsquo;t have to just build from the local machine, it&amp;rsquo;s just as straightforward to build from your GitHub repo as part of a CI/CD system. I&amp;rsquo;m not planning to go into any of that today, except I will force it to build for x86 since it is my plan to test on the homelab VM&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build --platform linux/amd64 -t iankulin/mdserver:latest .&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="the-registry"&gt;The Registry&lt;/h3&gt;
&lt;p&gt;So the images can be available to anyone, we need to make it available in a Docker Registry. The most famous one of these, and the one set up as the default for all the docker commands, is &lt;a href="https://hub.docker.com/"&gt;Docker Hub&lt;/a&gt;. Despite some &lt;a href="https://www.docker.com/blog/no-longer-sunsetting-the-free-team-plan/"&gt;missteps&lt;/a&gt;, it&amp;rsquo;s still the main place people and organisations store docker images.&lt;/p&gt;
&lt;p&gt;In order to push an image to a registry, we need to be signed in to it. As I&amp;rsquo;m using Docker Desktop, and I&amp;rsquo;m signed in to Docker Hub on that. I&amp;rsquo;ve skipped that step, but if you needed to, you&amp;rsquo;d use the &lt;a href="https://docs.docker.com/engine/reference/commandline/login/"&gt;docker login&lt;/a&gt; command. Once that&amp;rsquo;s sorted, the push is easy:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker push iankulin/mdserver:latest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.12.15-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.12.15-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this output, you can see some of the efficiencies of the layers - docker recognises (from the UUIDs) that the Alpine and Node layers are ones that I pulled down from it when I was creating the image locally, so it doesn&amp;rsquo;t send them back to Docker Hub.&lt;/p&gt;
&lt;p&gt;If we go to Docker Hub and search for mdserver, we should be able to find it now available to the public.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.10.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.10.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="using-the-image"&gt;Using the image&lt;/h3&gt;
&lt;p&gt;Now it&amp;rsquo;s in the registry, anyone can use it as easily as any of the Docker images - NGINX, Jellyfin - whatever. I provide a docker-compose file in the repo, it looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;
services:
 mdserver:
 image: iankulin/mdserver:latest
 ports:
 - &amp;#34;3000:3000&amp;#34;
 volumes:
 - ./public:/usr/src/app/public 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So any user can just drop that into a directory, and enter &lt;code&gt;docker compose up -d&lt;/code&gt; then the image will be pulled down and run, and they&amp;rsquo;ll have their server live.&lt;/p&gt;</description></item><item><title>Hosting Your Own Docker Registry</title><link>https://blog.iankulin.com/hosting-your-own-docker-registry/</link><pubDate>Mon, 25 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hosting-your-own-docker-registry/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/architectural-photography-of-cargo-containers-stack-hP4ZiN1_kdk?utm_content=creditShareLink&amp;utm_medium=referral&amp;utm_source=unsplash"&gt;&lt;img src="https://blog.iankulin.com/images/tri-eptaroka-mardiana-hp4zin1_kdk-unsplash.jpg" width="640" alt="Photo by Tri Eptaroka Mardianam on Unsplash
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Docker &lt;a href="https://docs.docker.com/subscription/core-subscription/details/"&gt;Personal (ie free tier) plan&lt;/a&gt; currently allows one private repository, but even if you want to pay for the next level where you can have unlimited repositories, you may still want to host your own private registry - it&amp;rsquo;s going to be quicker inside your network, and you won&amp;rsquo;t run up against Docker&amp;rsquo;s pull/push limits if you are hammering it with your CI/CD system.&lt;/p&gt;
&lt;p&gt;There are fancier tools, but in this post we&amp;rsquo;ll look at the basics of how to use the official registry app from Docker.&lt;/p&gt;
&lt;h3 id="initial-setup"&gt;Initial Setup&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://hub.docker.com/_/registry"&gt;registry app&lt;/a&gt; is (unsurprisingly) dockerised. So I&amp;rsquo;ve created a directory for the &lt;code&gt;docker-compose.yml&lt;/code&gt; file, and a &lt;code&gt;data&lt;/code&gt; sub directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-7.50.43-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And the yaml.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;services: registry: image: registry:2 container_name: registry restart: unless-stopped ports: - &amp;#34;5000:5000&amp;#34; environment: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data volumes: - ./data:/data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;docker compose up&lt;/code&gt;, and bingo. Our registry is live.&lt;/p&gt;
&lt;h3 id="creating-an-image"&gt;Creating an image&lt;/h3&gt;
&lt;p&gt;Now our registry is up, let&amp;rsquo;s jump over to another machine, and create an image to store in it. I&amp;rsquo;m only going to minimally explain this, since if you&amp;rsquo;re interested in your own registry, you&amp;rsquo;ve probably been down this path.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.24.50-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;FROM busyboxRUN mkdir /appCOPY script.sh /app/script.shWORKDIR /appRUN chmod +x script.shCMD [&amp;#34;./script.sh&amp;#34;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;script.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/shecho &amp;#34;Hello from Docker!&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So basically, this image contains a small Linux distro, and all it does is run a script that outputs &amp;ldquo;Hello from Docker!&amp;rdquo; to the console. We can build our image by switching into the directory with the &lt;code&gt;dockerfile&lt;/code&gt; and running:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker build -t hello-docker .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.37.15-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you want to run it to check my docker skills, use&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker run hello-docker
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="pushing--insecure"&gt;Pushing &amp;amp; Insecure&lt;/h3&gt;
&lt;p&gt;Now I want to push the image we&amp;rsquo;ve created to the new registry we set up earlier, but we&amp;rsquo;re going to run into a problem.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using two Debian virtual machines (LXCs actually) both on my homelab network. They&amp;rsquo;ve been named with Tailscale to make things clearer in the screenshots. (If you&amp;rsquo;re following along you&amp;rsquo;ll probably be using IP addresses). Importantly, there are no TLS certificates, self-signed or otherwise.&lt;/p&gt;
&lt;p&gt;First we need to tag our image to include the registry name:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker tag hello-docker:latest ct390-docker-reg:5000/hello-docker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.53.18-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;ll try to push it up to our registry with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker push ct390-docker-reg:5000/hello-docker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.35.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s happening is that Docker would (quite reasonably) prefer to only work over secure connections. We can override this on this machine for today&amp;rsquo;s demo purposes by adding an exception for our self-hosted registry. You&amp;rsquo;ll need to create the file &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; and add the registry that&amp;rsquo;s going to be allowed like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we restart docker and retry the push now, it should work:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.43.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That looks like it worked. If we wanted to check, we can just hit an endpoint on the registry:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl http://ct390-docker-reg:5000/v2/_catalog
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.49.36-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="pulling--insecure"&gt;Pulling &amp;amp; Insecure&lt;/h3&gt;
&lt;p&gt;Of course the ultimate test is going to be to use this image from a third machine, so let&amp;rsquo;s spin one up with a clean docker install with no images and try to run the image we&amp;rsquo;ve just added to our registry.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to have the same challenge pulling from a non-TLS registry as we had pushing to it, and the workaround is going to be exactly the same - add the registry to the insecure list in the &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;echo &amp;#39;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}&amp;#39; | sudo tee /etc/docker/daemon.jsonsudo systemctl daemon-reloadsudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we can run it. Since we don&amp;rsquo;t have the image locally yet, docker will pull it down for us from the registry before running it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-3.19.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. Our own private Docker registry to store our images.&lt;/p&gt;
&lt;h4 id="references"&gt;References&lt;/h4&gt;
&lt;p&gt;In writing this post, I relied on some these resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Digital Ocean - &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-private-docker-registry-on-ubuntu-20-04"&gt;How To Set Up a Private Docker Registry on Ubuntu 20.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Baeldung - &lt;a href="https://www.baeldung.com/ops/docker-private-registry"&gt;Configure a Private Docker Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;O&amp;rsquo;Reilly - &lt;a href="https://www.oreilly.com/library/view/kubernetes-in-the/9781492043270/app03.html"&gt;Configuring Docker to Push or Pull from an Insecure Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Beginning Node App Security</title><link>https://blog.iankulin.com/beginning-node-app-security/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/beginning-node-app-security/</guid><description>&lt;p&gt;Since I&amp;rsquo;m using Tailscale to painlessly manage all my networking on the homeserver here and my remotes, I&amp;rsquo;ve had the luxury of being a bit casual about the security of my internal apps and self hosted dev tools. I&amp;rsquo;m currently iterating on a web app that requires public access, and is therefore up on a VPS and exposed to all the evils of the open internet.&lt;/p&gt;
&lt;p&gt;I am in no way a security expert, but here&amp;rsquo;s a few of the (reasonably simple) steps I&amp;rsquo;ve taken to secure my node app.&lt;/p&gt;
&lt;h3 id="put-it-behind-nginx"&gt;Put it behind Nginx&lt;/h3&gt;
&lt;p&gt;I could just change the port number of the Node app to listen to port 80 and connect it directly to the world, but Nginx is battle hardened for outward facing tasks so that seems safer. Additionally, it opens up a lot of functionality such as putting my app on a subdomain and some other security options we&amp;rsquo;ll come to. Putting Nginx in front of your app like this is called &amp;lsquo;reverse proxying&amp;rsquo;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;	server_name sub.example.com;	location / { 		proxy_pass http://localhost:3000;			proxy_http_version 1.1;		proxy_set_header Upgrade $http_upgrade;		proxy_set_header Connection &amp;#39;upgrade&amp;#39;;		proxy_set_header Host $host;		proxy_cache_bypass $http_upgrade;	}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="basic-auth"&gt;Basic Auth&lt;/h3&gt;
&lt;p&gt;One of the Nginx options is the ability to turn on &amp;lsquo;&lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;basic auth&lt;/a&gt;&amp;rsquo;. This can be enabled for a route, subdomain, or a whole domain. It forces a user to authenticate before being able to access resources from that area. It&amp;rsquo;s basic in the sense that the password is manually set on the server in a file that the nginx conf points to. Ideally your app will have comprehensive auth built in, but (especially when you are still developing it) basic auth is a quick and easy way to prevent all of the internet from accessing your app.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;	server_name sub.example.com;	location / {		auth_basic &amp;#34;Secure app&amp;#34;;	 auth_basic_user_file /etc/nginx/.htpasswd;	}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="https"&gt;HTTPS&lt;/h3&gt;
&lt;p&gt;Enforcing SSL connections is much easier than it used to be (thank you L&lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;et&amp;rsquo;s Encrypt and Certbot&lt;/a&gt;) and it keeps all the data being sent between your app and the user&amp;rsquo;s browser encrypted - including the username and password you are using for your auth.&lt;/p&gt;
&lt;h3 id="logging"&gt;Logging&lt;/h3&gt;
&lt;p&gt;Ensuring that logs are turned on means that you&amp;rsquo;ve got some chance of detecting problems and possible attacks. In fact, you&amp;rsquo;ll probably be aghast at the number of bots that start accessing your server as soon as it&amp;rsquo;s live. For the most part, they are probing for the existence of known vulnerabilities in well known packages - may of the php. When I look up the IP addresses for these, they almost always come from China or Eastern Europe.&lt;/p&gt;
&lt;p&gt;Note that logging does involve some future maintenance. Logs need rotated and deleted or we&amp;rsquo;ll soon be running out of disk space.&lt;/p&gt;
&lt;h3 id="fail2ban"&gt;Fail2ban&lt;/h3&gt;
&lt;p&gt;Manually checking the logs is not effective, we need to automate this a bit. The things I&amp;rsquo;m looking for in my system is brute force attempts at breaking the basic auth, and the same with SSH.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/fail2ban/fail2ban"&gt;Fail2Ban&lt;/a&gt; can automate this (and many other things, but I&amp;rsquo;m just using these two) by scanning the logs for failed attempts. There are various settings to determine the thresholds - say check for 5 failed attempts in 10 minutes then ban the IP address for 30 minutes. Once the threshold is met, Fail2Ban updates iptables (the internal firewall) to block them.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@novel-ironic:~$ sudo fail2ban-client status sshdStatus for the jail: sshd|- Filter| |- Currently failed:	1| |- Total failed:	2960| `- File list:	/var/log/auth.log`- Actions |- Currently banned:	6 |- Total banned:	483 `- Banned IP list:	218.92.0.27 61.177.172.136 61.177.172.140 218.92.0.113 218.92.0.31 218.92.0.76ian@novel-ironic:~$ sudo fail2ban-client status nginx-http-authStatus for the jail: nginx-http-auth|- Filter| |- Currently failed:	1| |- Total failed:	6| `- File list:	/var/log/nginx/error.log`- Actions |- Currently banned:	0 |- Total banned:	1 `- Banned IP list:	ian@novel-ironic:~$ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the output above, you can see there are 6 ip addresses currently blocked for trying to crack SSH, and there&amp;rsquo;s been 483 banned in the couple of days since I turned it on - so this is a very common attack vector. The basic auth one just has a single ban (from when I tested it). I&amp;rsquo;m not sure why I like looking at the list of bans so much, but I do!&lt;/p&gt;
&lt;h3 id="cloud-firewall"&gt;Cloud Firewall&lt;/h3&gt;
&lt;p&gt;Many VPS providers will have a cloud firewall (although they may call it something else). We can use this to lock down all the ports we are not using to massively reduce the attack surface for this machine. One of the very appealing things about this firewall which is external to the VPS is that it&amp;rsquo;s accessed via the VPS provider web interface - so it&amp;rsquo;s not possible to lock yourself out if you make a mistake - as opposed to when you&amp;rsquo;re SSHd in and fiddling with iptables.&lt;/p&gt;
&lt;p&gt;Since this VPS is just running web apps, I just have ports 80, 443 and 22 open.&lt;/p&gt;
&lt;h3 id="no-root-login"&gt;No root login&lt;/h3&gt;
&lt;p&gt;By default, Ubuntu does not allow root login by password, but once I&amp;rsquo;ve added a new user and added them to the sudo group, I turn it off entirely. Most of those SSH attempts that failed would have been trying common user names including root, so may as well take it out of the possibilities.&lt;/p&gt;
&lt;h3 id="ssh-keys"&gt;SSH keys&lt;/h3&gt;
&lt;p&gt;Apart from being more convenient, well managed SSH keys are much safer than using passwords. So my new user copies up their keys and I set that user to no login with password as well.&lt;/p&gt;
&lt;h3 id="updates"&gt;Updates&lt;/h3&gt;
&lt;p&gt;One of the wonderful things about open source and the modern web, is that as new vulnerabilities are being discovered, they are being patched. We only get them if we run updates though. It&amp;rsquo;s possible (and recommended) to use &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-keep-ubuntu-20-04-servers-updated"&gt;automatic updates&lt;/a&gt;, but I have a weekend ansible routine to do them that I like to look at the output of to check everything&amp;rsquo;s healthy.&lt;/p&gt;
&lt;h3 id="monitoring"&gt;Monitoring&lt;/h3&gt;
&lt;p&gt;I use a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;very simple monitoring system&lt;/a&gt; for all the VM&amp;rsquo;s and containers in my Tailnet - just checking the root disk space and available memory. This is exposed as an http endpoint, then checked by &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;. That&amp;rsquo;s better than nothing, but for a production system not really enough. This is one of the areas I&amp;rsquo;ll revisit in the future.&lt;/p&gt;</description></item><item><title>Fly.io, Uptime Kuma &amp; scraping a status page</title><link>https://blog.iankulin.com/fly-io-uptime-kuma-scraping-a-status-page/</link><pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fly-io-uptime-kuma-scraping-a-status-page/</guid><description>&lt;p&gt;&lt;a href="https://dribbble.com/shots/5657880-Fly-io-Logo"&gt;&lt;img src="https://blog.iankulin.com/images/c1fef772e2dca5e1ab8c812f465c95a8.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been aware since I set up &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt; for my monitoring, that having an instance on my local network monitoring my VPS websites wasn&amp;rsquo;t ideal. The main reason being that the flakiest part of my infrastructure is my 4G home internet, so if that goes down I have no website monitoring, and even if I did, the notifications couldn&amp;rsquo;t get out.&lt;/p&gt;
&lt;p&gt;Of course, it would also be a simple matter to run an instance on the VPS that I host the sites on, but that has a similar problem in that if the VPS goes down, so does my monitoring of the VPS. What I really need is a third, independent space to run an instance.&lt;/p&gt;
&lt;h3 id="uptime-robot"&gt;Uptime Robot&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://uptimerobot.com/"&gt;Uptime Robot&lt;/a&gt; is a monitoring service that seems somehow related to Uptime Kuma? They have some of the same terminology and colour schemes - so I&amp;rsquo;m not really sure. Perhaps it&amp;rsquo;s a fork, or perhaps Uptime Kuma was inspired by Robot. Robot does have an API which is a nice addition, since ideally if my monitoring is spread around, I&amp;rsquo;d like to pull it all back into one &amp;lsquo;pane of glass&amp;rsquo; by having my system monitor the remote for how many &amp;lsquo;down&amp;rsquo; sites it&amp;rsquo;s tracking. It also has a number of other extra features such as heartbeat monitoring.&lt;/p&gt;
&lt;p&gt;Uptime Robot is a paid service, but like nearly all VC funded things growing a user base it has a free tier with some restrictions. I like NTFY for my notifications, but on Robot I could only access email notifications. There are iOS and Android apps, but I didn&amp;rsquo;t try them.&lt;/p&gt;
&lt;h3 id="third-space"&gt;Third Space&lt;/h3&gt;
&lt;p&gt;Ideally, I like to run another Uptime Kuma in a VPS on a different provider. I&amp;rsquo;ve heard that &lt;a href="https://www.oracle.com/au/cloud/free/"&gt;Oracle have a free tier&lt;/a&gt; which seems like it would be fine for this application, but a more interesting idea that I&amp;rsquo;ve been thinking of using for other projects is Fly.io.&lt;/p&gt;
&lt;h3 id="flyio"&gt;Fly.io&lt;/h3&gt;
&lt;p&gt;Fly.io own physical servers in colo datacentres around the world on which they offer compute based on &lt;a href="https://www.amazon.science/blog/how-awss-firecracker-virtual-machines-work"&gt;Firecracker VM&amp;rsquo;s&lt;/a&gt;. The cute bit is that you give them a Docker container, and they unpack it into one of these fast baby VM&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;The exact nature of their &amp;lsquo;free tier&amp;rsquo; is hard to figure out from their &lt;a href="https://fly.io/docs/about/pricing/"&gt;pricing page&lt;/a&gt;, but based on &lt;a href="https://community.fly.io/t/fly-io-free-tier-billing/11432"&gt;some answers to questions in their forum&lt;/a&gt;, and &lt;a href="https://jfmadrid.notion.site/Uptime-Kuma-for-Free-on-Fly-io-e5eeead6dfb4425b8403c100ec986191"&gt;blog posts from others who have set up Uptime Kuma&lt;/a&gt; there, it sounds like the deal is that if you use one shared CPU &lt;em&gt;and&lt;/em&gt; keep your storage under 3GB &lt;em&gt;and&lt;/em&gt; the charges for your use add up to less than $5/month - then it&amp;rsquo;s free. I did have to provide credit card details, so if &lt;a href="https://www.youtube.com/watch?v=N6lYcXjd4pg"&gt;I get a $71,393 bill,&lt;/a&gt; I&amp;rsquo;ll come back here and edit this. (&lt;em&gt;edit from the future: eight months later I haven&amp;rsquo;t paid a cent&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;To get Uptime Kuma running on Fly.io, I followed &lt;a href="https://jfmadrid.notion.site/Uptime-Kuma-for-Free-on-Fly-io-e5eeead6dfb4425b8403c100ec986191"&gt;this guide&lt;/a&gt;, but the steps where basically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an account on Fly.io&lt;/li&gt;
&lt;li&gt;Install the Fly.io command line tools and run a command to &amp;lsquo;create&amp;rsquo; your app&lt;/li&gt;
&lt;li&gt;Create a &amp;lsquo;&lt;a href="https://github.com/lubien/fly-uptime-kuma/blob/main/fly.toml"&gt;fly.toml&lt;/a&gt;&amp;rsquo; file which is a text config file pointing to the docker image and supplying some details such as ports and location&lt;/li&gt;
&lt;li&gt;Use the CLI to set the disk space needed, and &amp;lsquo;deploy&amp;rsquo; the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was impressive how simple all this was. If the intention of the free tier is to get you to try it, and show you how painless it is to deploy any dockerised app to the edge, then mission accomplished.&lt;/p&gt;
&lt;p&gt;You can check on the status of your app at &lt;a href="https://fly.io/dashboard"&gt;https://fly.io/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-6.31.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-6.31.22-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And go to &lt;appname&gt;.fly.dev to see your app. On the free tier, you&amp;rsquo;re on a shared IPV4 address but it is possible to use your own domain if desired - that&amp;rsquo;s one of the things to set up in the .toml file.&lt;/p&gt;
&lt;p&gt;It is remarkable what you can deploy for free in the golden age of venture capital.&lt;/p&gt;
&lt;h3 id="extracting-status"&gt;Extracting Status&lt;/h3&gt;
&lt;p&gt;One of Uptime Kuma&amp;rsquo;s functions is to provide public (ie viewable without being logged in) &amp;lsquo;status&amp;rsquo; pages, and if all the services you&amp;rsquo;ve added to that status group are up, it has. great big heading saying &amp;ldquo;All Systems Operational&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.38.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.38.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So my plan to pull this status into my homelab instance of Uptime Kuma was just to add this remote status page as a monitor, and search for the keyword &amp;lsquo;All Systems Operational&amp;rsquo;. If that was found, I&amp;rsquo;d know everything was good. But of course, this is a modern web-app (I think using &lt;a href="https://vuejs.org/"&gt;Vue&lt;/a&gt;), so that text does not exist in the page, it&amp;rsquo;s added to the DOM by some JavaScript after the page is loaded based on some client side processing of (probably) some JSON data it pulls in.&lt;/p&gt;
&lt;p&gt;One option would be to use a web scraping library to write something to access this piece of information. On a page like this, that would involve a headless browser rendering the DOM then exposing it.&lt;/p&gt;
&lt;p&gt;But of course, the Javascript that is building the page we&amp;rsquo;re looking at is getting its data from somewhere, so it&amp;rsquo;s probably easier for us to grab that data directly and process it ourselves. How do we see where the data is from? We use the browser tools to look at the network requests when the page is loaded.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.20.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.20.50-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So if you view the status page at &lt;code&gt;&amp;lt;whatever.com&amp;gt;/status/&amp;lt;page_name&amp;gt;&lt;/code&gt;, it loads some data from &lt;code&gt;&amp;lt;whatever.com&amp;gt;/api/status-page/heartbeat/&amp;lt;page_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The JSON that&amp;rsquo;s returned from this request contains two objects: &lt;code&gt;heartbeatlist&lt;/code&gt;, and &lt;code&gt;uptimelist&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.06.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.06.05-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heartbeatlist&lt;/code&gt; contains the last 50 retrievals for each of the URL&amp;rsquo;s being monitored. Each of these retrievals has a status (1 for up, 0 for down) and the response time. &lt;code&gt;uptimelist&lt;/code&gt; is the fraction of uptime. You can see in the data above that the first URL has a lower percentage of up-time (because I failed it to check my understanding of the status data).&lt;/p&gt;
&lt;p&gt;So I need to write an endpoint that requests this data, then checks the last array element of each of the URLs in the heartbeat list, then spit out some text saying if all the URL&amp;rsquo;s in this status group are available. That&amp;rsquo;s quite doable, I have the skills, but it&amp;rsquo;s probably a two hour job to do properly.&lt;/p&gt;
&lt;p&gt;Since this is an open source project, a better use of that time would be to add this functionality to Uptime Kuma so it would be available to anyone with the same problem. It might be a niche case, but the code to provide this output would be simpler inside the project and much more durable than reverse engineering it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the source and see what it&amp;rsquo;s like.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.34.24-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.34.24-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, well well. What do we have here? There&amp;rsquo;s an api route that outputs an SVG badge for a status page. The badge says &amp;lsquo;Degraded&amp;rsquo; in amber if some of the URL&amp;rsquo;s are down, and &amp;lsquo;Up&amp;rsquo; in green if they are all up. Those words are present in an aria label and the svg &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag, so they&amp;rsquo;ll be detectable by the Uptime Kuma &amp;lsquo;keyword&amp;rsquo; search.&lt;/p&gt;
&lt;p&gt;Five minutes later, we&amp;rsquo;re in business. Thank you open source!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.41.52-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.41.52-pm.png" width="772" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Getting Your Vite React App to Work on Github Pages</title><link>https://blog.iankulin.com/getting-your-vite-react-app-to-work-on-github-pages/</link><pubDate>Fri, 26 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-your-vite-react-app-to-work-on-github-pages/</guid><description>&lt;img src="https://blog.iankulin.com/images/combined.png" width="512" alt=""&gt;
&lt;p&gt;One of the many cool things about GitHub is &lt;a href="https://pages.github.com"&gt;GitHub Pages&lt;/a&gt; - the free web hosting Microsoft gives you while they vacuum up &lt;a href="https://docs.github.com/en/copilot/overview-of-github-copilot/about-github-copilot-individual"&gt;your code for CoPilot&lt;/a&gt; training. Each repository you keep there can have pages at &lt;code&gt;&amp;lt;your-github-username&amp;gt;.github.io/&amp;lt;repo-name&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="github"&gt;GitHub&lt;/h3&gt;
&lt;p&gt;To enable this, you need to go into the settings for the repository - look down the left for &amp;ldquo;Pages&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-1.58.05-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have it based on a complicated GitHub action (where your build step happens on GitHub when you push your code), but the easiest thing is just to have it deployed from a branch. To do this you choose which branch (usually main) and whereabouts in the main branch your HTML is. The choices are in the root of your project, or in the &lt;code&gt;/docs&lt;/code&gt; directory. I&amp;rsquo;ve chosen the &lt;code&gt;/docs&lt;/code&gt; directory in the screenshot above, since my messy React project is in the root.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all the GitHub set up we need. Now whenever I push my project to the &lt;code&gt;main&lt;/code&gt; branch on GitHub, whatever is in the &lt;code&gt;/docs&lt;/code&gt; directory will be uploaded to my GitHub page for this repo.&lt;/p&gt;
&lt;h3 id="vitereact"&gt;Vite/React&lt;/h3&gt;
&lt;p&gt;Now we need to make a couple of changes to our project to get this to work. The first is to tell Vite the &amp;ldquo;base directory&amp;rdquo; for the project which needs to be the repo name you&amp;rsquo;ve used on GutHub.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.04.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.04.50-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is written into the &lt;code&gt;index.html&lt;/code&gt; that is built as part of this process. If it&amp;rsquo;s not there, then any browser accessing your &lt;code&gt;index.html&lt;/code&gt; on gh-pages won&amp;rsquo;t be able to find your JavaScript, and the user will be left looking at a blank white page instead of your amazing app.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.11.06-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.11.06-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My process from this point, is to build the project with &lt;code&gt;npm run build&lt;/code&gt;. By default, this creates a &lt;code&gt;/dist&lt;/code&gt; directory in your project (which is already added to &lt;code&gt;.gitignore&lt;/code&gt;) and puts the project artifacts (the HTML, JavaScript, CSS and any images) into it. I then manually copy the artifacts over to the &lt;code&gt;/docs&lt;/code&gt; directory of the project and push it up to GitHub to be published - which takes two or three minutes.&lt;/p&gt;
&lt;p&gt;I like this manual step of copying the files over so that publishing is an intentful action on my part, and also, for solo projects I generally just work out of the main branch rather than on feature branches that then get PR&amp;rsquo;d into main. If you did want the process to be more CI/CD flavoured, you can just make another change the &lt;code&gt;vite.config.ts&lt;/code&gt; file to have your builds go straight to the &lt;code&gt;/docs&lt;/code&gt; folder.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { defineConfig } from &amp;#39;vite&amp;#39;import react from &amp;#39;@vitejs/plugin-react&amp;#39;// https://vitejs.dev/config/export default defineConfig({ base: &amp;#34;/mosh-expense/&amp;#34;, plugins: [react()], build: { outDir: &amp;#39;docs&amp;#39;, }})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once all that&amp;rsquo;s working, and you&amp;rsquo;ve pushed your changes and waited a minute or two, your project should be live to the world on &lt;code&gt;github.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.45.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.45.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you want users browsing your repo to find the live version, it&amp;rsquo;s worth editing your repository about settings to point to it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.47.30-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-4.47.30-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Using LXC templates in Proxmox</title><link>https://blog.iankulin.com/using-lxc-templates-in-proxmox/</link><pubDate>Sun, 24 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-lxc-templates-in-proxmox/</guid><description>&lt;p&gt;I wrote a couple of weeks ago about a &lt;a href="https://blog.iankulin.com/new-self-hosted-service-workflow/"&gt;standard workflow&lt;/a&gt; I use to spin up a web service in an LXC container to add to my self-hosted collection of services. It went a bit like: do this, and then this, then this other thing. Whenever you find yourself repeating a set of steps like this, it&amp;rsquo;s usually a sign that you should be automating it. Not just to save time (although this is a key benefit) but also to improve repeatability and to avoid introducing errors.&lt;/p&gt;
&lt;p&gt;In Proxmox, this particular task is easily systematized using container &lt;em&gt;templates&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The simplest way to think of a container template is that it&amp;rsquo;s just a one-for-one snapshot of a container (ie the disk image, the configuration file that contains all the VM hardware information) all squashed up into a tarball - basically the same as a backup. This is then copied to create new containers.&lt;/p&gt;
&lt;p&gt;If we create new containers from a template, all the software and configuration that was in the template will be present in the new container. This is obviously the desired behaviour, but it presents some issues - we probably don&amp;rsquo;t want multiple containers with the same host name, or MAC address, or SSH host keys. Some of these issues Proxmox will sort out for us, some we&amp;rsquo;ll need to tidy up manually.&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Issue&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Host name&lt;/td&gt;&lt;td&gt;When you 'clone' the template in Proxmox, it will ask you the new host name.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MAC address&lt;/td&gt;&lt;td&gt;Proxmox just creates a new one with no input needed from you.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Machine ID&lt;/td&gt;&lt;td&gt;If you truncate it in the template before you save it as a template, a new one will be created then the container is.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SSH host keys&lt;/td&gt;&lt;td&gt;Manually delete them in the template before saving the template, then manually re-create them in the new container once it's booted up.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id="making-the-template"&gt;Making the template&lt;/h3&gt;
&lt;p&gt;Create an LXC container as normal - ie chose &amp;ldquo;Create CT&amp;rdquo; in Proxmox, give it a name, choose a password, then a template, make the decisions about memory, disk, networking etc. Note that when you are choosing an official template to create it from (Apline, Debian, Ubuntu etc) , these files are almost identical to what we&amp;rsquo;ll be creating in this process.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s up and running, I &lt;code&gt;ssh&lt;/code&gt; in and run all my apt updates and install any software or make any other changes. For me this includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Making it a client of &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;my local apt-cache&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;running ssh update and upgrades&lt;/li&gt;
&lt;li&gt;Copying in my SSH keys (ssh-copy-id)&lt;/li&gt;
&lt;li&gt;Installing sudo and adding myself as a sudo user&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/install/debian/"&gt;Installing Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailscale.com/kb/1174/install-debian-bookworm/"&gt;Installing Tailscale,&lt;/a&gt; and doing the &lt;a href="https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/"&gt;Tailscale LXC fix&lt;/a&gt; (but not running &lt;code&gt;tailscale up&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Installing &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;my simple machine status server&lt;/a&gt; that&amp;rsquo;s used for monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s all done, we&amp;rsquo;ve got a nice clean container, but with all the software and config that we need for most future containers.&lt;/p&gt;
&lt;p&gt;Now we need to address a couple of the issues that could be caused by cloning this LXC from the table above.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Machine ID - you could probably get away with not worrying about this, but might run into a confusing issue later. A simple &lt;code&gt;sudo truncate -s 0 /etc/machine-id&lt;/code&gt; will nuke it, then a new unique one will be created when the clone container boots up.&lt;/li&gt;
&lt;li&gt;SSH host keys - you know when you ssh into a new system for the first time and OpenSSH asks you if you&amp;rsquo;re sure you want to recognise this server? This is done by the server identifying itself with one of these keys. If these are left the same for all of the clones of our template, you&amp;rsquo;ll have to be constantly deleting the keys out of your &lt;code&gt;known_hosts&lt;/code&gt; file. We can delete them now (which will make this template and any clones impossible to &lt;code&gt;ssh&lt;/code&gt; into) or later. I choose now. &lt;code&gt;sudo rm /etc/ssh/ssh_host_*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once this is all done, we are ready to convert this container into a template. Shut it down, then if you are cautious, back it up (you can&amp;rsquo;t convert a template back into a container). Then right click on it in Proxmox and choose &amp;lsquo;Convert to Template&amp;quot;. After a few seconds, it will be in your server view as a template with a slightly different icon.&lt;/p&gt;
&lt;h3 id="using-the-template"&gt;Using the template&lt;/h3&gt;
&lt;p&gt;The process of using our new template is called cloning. Right click on the template in Proxmox, and choose clone. You&amp;rsquo;ll be presented with a dialogue to give it a number, choose a host name, select the clone type (you want a &amp;lsquo;full clone&amp;rsquo;) and where this container&amp;rsquo;s storage will be.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-03-at-12.43.10-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-03-at-12.43.10-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few seconds later the new LXC container will be in your server view and can be started.&lt;/p&gt;
&lt;p&gt;You won&amp;rsquo;t be able to ssh into this container yet as we deleted the host keys. Use the console in Proxmox to log in (with the root or sudo user credentials you set up earlier) and recreate the ssh host keys with &lt;code&gt;sudo dpkg-reconfigure openssh-server&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;While you are here, you should probably change the passwords for both users with &lt;code&gt;passwd&lt;/code&gt; or &lt;code&gt;sudo passwd &amp;lt;username&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The other thing I&amp;rsquo;ll need to do to use my container with Tailscale is to run &lt;code&gt;sudo tailscale up&lt;/code&gt; and complete the steps for that.&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;re done. You&amp;rsquo;ve now got a container that&amp;rsquo;s identical to our template, except for the things that need to be different. You can go ahead and use it as needed now.&lt;/p&gt;
&lt;h4 id="resources"&gt;Resources&lt;/h4&gt;
&lt;p&gt;Here&amp;rsquo;s a couple of useful things I came across in the writing of this post:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=J29onrRqE_I&amp;amp;t=619s"&gt;Proxmox VE Full Course: Class 8&lt;/a&gt; - Creating Container Templates - video from Jay (Learn Linux TV)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pve.proxmox.com/wiki/Linux_Container"&gt;Linux Containers&lt;/a&gt; - from the Proxmox docs&lt;/p&gt;</description></item><item><title>Gogs, Gitea, Forgejo</title><link>https://blog.iankulin.com/gogs-gitea-forgejo/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gogs-gitea-forgejo/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_7071-1.png" width="640" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been really pleased with &lt;a href="https://blog.iankulin.com/tags/gogs/"&gt;Gogs&lt;/a&gt; - it&amp;rsquo;s lightweight, was simple to spin up, and has worked perfectly. But then this morning on Mastodon, there&amp;rsquo;s a &lt;a href="https://mastodon.social/@Codeberg@social.anoxinon.de/111471407276450348"&gt;post from @Codeberg.org&lt;/a&gt; describing a security vulnerability in their Git hosting project Forgejo. This issue also apparently affects Gitea and Gogs - what&amp;rsquo;s up with that?&lt;/p&gt;
&lt;p&gt;I actually already did spend a bit of time comparing Gogs and Gitea before deciding on Gogs, since I&amp;rsquo;d heard of people running Gitea over the past year or so, but only seen that Gogs seemed to be popular with self-hosters in a Lemmy post I&amp;rsquo;d read. My first impression was that Gitea was more focused on CI/CD and seemed to have a more complicated install process.&lt;/p&gt;
&lt;p&gt;What I didn&amp;rsquo;t do, was think about the project management and teams. It turns out that &lt;a href="https://about.gitea.com/"&gt;Gitea&lt;/a&gt; was forked from &lt;a href="https://gogs.io/"&gt;Gogs&lt;/a&gt; by contributors in 2016 due to &lt;a href="https://blog.gitea.com/welcome-to-gitea/"&gt;disagreements about the project management&lt;/a&gt;. Then at the end of 2022 &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; was forked from Gitea due to &lt;a href="https://forgejo.org/2022-12-15-hello-forgejo/"&gt;Gitea moving the trademarks and domain into a company&lt;/a&gt; providing Gitea support.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://forgejo.org/2023-11-release-v1-20-5-1/"&gt;CVE announcement from Forgeo&lt;/a&gt;, while a little snarky about their ancestors, does give the impression of a functional organisation that&amp;rsquo;s able to deal with issues as they come up. It&amp;rsquo;s a credit to the group to be in that position after just a year, and their &lt;a href="https://codeberg.org/forgejo/forgejo"&gt;repo&lt;/a&gt; (which is dogfooded) seems plenty active.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve only just started on Gogs, so it&amp;rsquo;s still easy to move if that&amp;rsquo;s what I decide. I guess my learning from stumbling upon this security announcement is more that I should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;take into account more than just project features when making these decisions&lt;/li&gt;
&lt;li&gt;I need to be subscribed to the channels where I&amp;rsquo;d learn about security issues in the projects I&amp;rsquo;m using and their major dependencies.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Git - pushing to two remotes</title><link>https://blog.iankulin.com/git-pushing-to-two-remotes/</link><pubDate>Fri, 15 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-pushing-to-two-remotes/</guid><description>&lt;p&gt;I am loving running a local Gogs instance - it&amp;rsquo;s nice pushing my git repos to a totally private hub that I know is backed up with all my other self-hosted infrastructure.&lt;/p&gt;
&lt;p&gt;Of course, there&amp;rsquo;s good reasons to have code in GitHub as well - my build-in-public philosophy, the vague possibility that some of it might be useful to someone, my contribution to our future AI overlords, and when I need to make some code linkable - for example from one of these posts. And of course there&amp;rsquo;s this bit of social-engineering which I assume was inspired by the bathroom decor in &lt;a href="https://i.pinimg.com/originals/94/23/85/9423854153f55938c454a061ad5462fe.gif"&gt;Veronica Mars&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-5.45.50-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Git is an amazing tool, so of course this is possible. Normally my workflow is that I &lt;code&gt;git init&lt;/code&gt; whenever I&amp;rsquo;m working on a new something, then at some point I think &amp;ldquo;I should really push all this so it&amp;rsquo;s backed up&amp;rdquo;. I create the repository for it on GitHub or Gogs via the web interface, then come back to my project and:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git remote add origin git@github.com:IanKulin/test.git&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is making the connection between my local project and the GitHub repo. I&amp;rsquo;d never really thought about what &lt;code&gt;origin&lt;/code&gt; meant in this context the hundreds of times I&amp;rsquo;ve previously typed it in, but actually it&amp;rsquo;s just the name we are giving to this connection. It&amp;rsquo;s just a convention to call it &amp;lsquo;origin&amp;rsquo;, it could just as easily be called &amp;lsquo;fred&amp;rsquo; or &amp;lsquo;github&amp;rsquo;. Since I am now planning to push to two separate remotes, it&amp;rsquo;s going to make sense to give them meaningful names. So in that case, we can do this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git remote add github git@github.com:IanKulin/test.git
git remote add gogs http://ct-gogs/iankulin/test.git
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, we can push with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push github main
git push gogs main
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You might be wondering what happens if you just do a &lt;code&gt;git push&lt;/code&gt; at this stage (or as I like to call it &amp;ldquo;&lt;em&gt;Pressing the &amp;lsquo;Publish Branch&amp;rsquo; button on the VS Code source control panel&amp;rdquo;&lt;/em&gt;). The answer is that at the command line you&amp;rsquo;ll get an error saying you haven&amp;rsquo;t specified the destination, or in VS Code, it will ask you which one.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-9.41.08-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We can set the default remote with the -u flag when we&amp;rsquo;re pushing&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push -u gogs main
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-9.46.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now the button in VS Code will say something &amp;ldquo;Sync Changes&amp;rdquo; and when you press it, it will only push to the remote we used in the last &lt;code&gt;-u&lt;/code&gt; push. Same thing if we &lt;code&gt;git push&lt;/code&gt; at the command line - it will work, but only push to the remote we used in the last &lt;code&gt;-u&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also worth noting that when we&amp;rsquo;ve set the default remote with the &lt;code&gt;-u&lt;/code&gt; flag in a &lt;code&gt;push&lt;/code&gt;, it is also the default remote for pulling from. Essentially this remote becomes the source-of-truth.&lt;/p&gt;
&lt;p&gt;For me this setup is usually fine - I&amp;rsquo;m generally working on my local gogs remote, that&amp;rsquo;s the source of truth so I specify it as the default with the &lt;code&gt;push -u&lt;/code&gt;. Then, when I&amp;rsquo;m done, I manually push to github so I can share it. If it was a project I needed to work on with anyone else, that would have to be the other way around - I&amp;rsquo;d use GitHub (or GitLab, Bitbucket etc) as the source of truth, and probably not even worry about hosting a copy on my home network unless I was worried about the repo being deleted.&lt;/p&gt;</description></item><item><title>New Self-Hosted Service Workflow</title><link>https://blog.iankulin.com/new-self-hosted-service-workflow/</link><pubDate>Sun, 03 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/new-self-hosted-service-workflow/</guid><description>&lt;p&gt;I&amp;rsquo;ve developed a bit of a workflow for setting up a new service of some type on the homelab. Installing it is the obvious thing, but I also have a few quality of life things I do to make it a full production-quality part of my installation. I thought it might be helpful to run through those things using a recent example of adding &lt;a href="https://www.audiobookshelf.org/"&gt;audiobookshelf&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="audiobookshelf"&gt;audiobookshelf&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.audiobookshelf.org/"&gt;audiobookshelf&lt;/a&gt; is a web based system for viewing, playing, downloading and/or generally managing your audio books. I&amp;rsquo;ve been an &lt;a href="https://www.audible.com.au/"&gt;Audible&lt;/a&gt; user/subscriber, but recently got grumpy at them about something - I think I had paused my subscription, and my downloaded books were still available on my phone. I was halfway through one, upgraded the app, and then wasn&amp;rsquo;t able to play the book without re-subscribing. That might not be exactly right, but it was some type of frustrating carry on like that.&lt;/p&gt;
&lt;p&gt;In any case, that made me decide I couldn&amp;rsquo;t trust them, and it was time to reassert my digital sovereignty by downloading the books I&amp;rsquo;d paid for (and the ones they&amp;rsquo;d given me), removing the &lt;a href="https://en.wikipedia.org/wiki/Digital_rights_management"&gt;DRM&lt;/a&gt;, and hosting it myself. The first two steps of that process were easily carried out with a brilliant bit of software called &lt;a href="https://openaudible.org/"&gt;OpenAudible&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="do-it-on-dev"&gt;Do it on dev&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/img_7003.jpg" width="900" alt=""&gt;
&lt;p&gt;Since I have the luxury of having separate production and development servers, I generally play around with new things I&amp;rsquo;m trying out on the dev instance of Proxmox. Note that this is almost entirely unnecessary - since everything is virtualised in Proxmox on the production server, there&amp;rsquo;s hardly any damage I could cause in one VM or container that would adversely affect anything else.&lt;/p&gt;
&lt;p&gt;Nevertheless, whether it&amp;rsquo;s caution, or a need to justify the size of the homelab, I always start building new things on the dev server. Once it&amp;rsquo;s all working perfectly, it&amp;rsquo;s a simple matter (that we&amp;rsquo;ll get to later) to move it as-is to the production server.&lt;/p&gt;
&lt;h3 id="installation-stack"&gt;Installation Stack&lt;/h3&gt;
&lt;p&gt;My default setup now is a Docker container, inside an LXC container on Proxmox. Although this originally felt like a comical number of levels of abstraction, each layer is doing something for me, and now it just feels like the cost of doing business.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox - virtualising everything insulates services from each other, makes moving them around easier, backing them up and restoring them trivial, and provides a level of high availability.&lt;/li&gt;
&lt;li&gt;LXC - lighter than a full VM, more VM like than Docker, and quicker to play with. Does add a bit of complexity we&amp;rsquo;ll get to later.&lt;/li&gt;
&lt;li&gt;Docker - OCI compliant containers are the bomb. This is how we do software now. I pushed back as long as I could but the logic is too strong. There are problems still to solve around &lt;a href="https://www.cisa.gov/sbom"&gt;SBOM&lt;/a&gt;, but the reduction in the work of managing installations is compelling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I create a non-root user, and the &lt;code&gt;docker-compose.yml&lt;/code&gt; and the directories for any config or data all go in that user&amp;rsquo;s home directory. I don&amp;rsquo;t prefer &lt;a href="https://docs.docker.com/storage/volumes/"&gt;Docker volumes&lt;/a&gt; for the data any more since the &lt;a href="https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/"&gt;downsides&lt;/a&gt; annoy me and the upsides must be in order to solve problems I haven&amp;rsquo;t encountered yet.&lt;/p&gt;
&lt;p&gt;Since there are a few little gotchas using LXC, when I&amp;rsquo;m trying something for the very first time, and I&amp;rsquo;m not even sure if it&amp;rsquo;s going to end up being used, I&amp;rsquo;ll do it in an VM first. I have a bunch of VM&amp;rsquo;s on the dev machine in varying states, so I normally pick one of them that already had Docker installed. This also gives me an idea for the amount of RAM and disk space the container is going to need. Changing the memory size once it&amp;rsquo;s in production is no biggie, but expanding the disk space is a bit of stuffing around.&lt;/p&gt;
&lt;p&gt;When I&amp;rsquo;m ready to make the container, it&amp;rsquo;s always the latest Debian stable, unprivileged, nesting turned on. Very few web services require more than 1GB RAM, and I guess the disk usage from the earlier trials then add a bit. I have lots of disk space and CPU time - it&amp;rsquo;s usually memory that&amp;rsquo;s the first bottleneck you&amp;rsquo;ll run into on little homelab servers. I&amp;rsquo;m sure I&amp;rsquo;ve heard &lt;a href="https://2.5admins.com/"&gt;Jim Salter and Allan Jude&lt;/a&gt; recommend that you should keep the VM memory low to leave more for the host so the it can effectively cache for all the guests.&lt;/p&gt;
&lt;p&gt;I always use docker-compose. Too many times I&amp;rsquo;ve wanted to upgrade a container, and have to waste time figuring out what the run command was. The compose file is good documentation for where your data is as well if you are, like me, avoiding volumes.&lt;/p&gt;
&lt;h3 id="the-steps"&gt;The Steps&lt;/h3&gt;
&lt;h4 id="some-installs"&gt;Some installs&lt;/h4&gt;
&lt;p&gt;With the fresh LXC created (latest Debian stable, unprivileged, nesting turned on), and started, I use the Proxmox console to log in, do some &lt;code&gt;apt&lt;/code&gt; updates, use &lt;code&gt;adduser&lt;/code&gt; to add my user, &lt;code&gt;apt install sudo&lt;/code&gt; and then &lt;code&gt;usermod&lt;/code&gt; to add my user to the sudo group.&lt;/p&gt;
&lt;p&gt;I then switch to a real terminal and ssh in as that user to install Docker. While that&amp;rsquo;s happening, I log into my router and reserve the IP address for the new container. This will follow when I move the container to the production server since it takes it&amp;rsquo;s MAC address with it.&lt;/p&gt;
&lt;p&gt;My pattern for SSH keys, which might not be the most secure, is that I have a key per device. So there&amp;rsquo;s one from my laptop, one for the terminal on my phone, and one for a VM that I sometimes use as an entry point to my home network via Tailnet. My theory with all this is that if any of those devices are compromised (for example my laptop is stolen) I can revoke that key from each of my services.&lt;/p&gt;
&lt;h4 id="nas-mount"&gt;NAS Mount&lt;/h4&gt;
&lt;p&gt;Often the service I&amp;rsquo;m installing needs access to the NAS - and that&amp;rsquo;s the case for audibookshelf which obviously needs access to my collection of audio books on my four bay Synology. I use an &lt;code&gt;/etc/fstab&lt;/code&gt; entry to mount the folder I&amp;rsquo;m interested in. I&amp;rsquo;ve set up the NAS to share these over SMB. The entry for audiobookshelf looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.32/media/books/audio/ /mnt/media cifs username=abs_user,password=SeCrErpaSSword,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a bit going on here, let&amp;rsquo;s pull it apart:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;//192.168.100.32/media/books/audio/&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The directory on the NAS where my audiobooks are stored. I&amp;rsquo;ve been a bit slack here. It would have been better for that directory to have been it&amp;rsquo;s own share to reduce the attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;/mnt/media&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the directory in the LXC container that we&amp;rsquo;re mounting the books to. If I could go back in time to when I started by Linux &amp;amp; self-hosting journey, I would not have used the word media, since in Linux that more refers to things like USB drives and less like entertainment to consume. &lt;a href="https://www.karlton.org/2017/12/naming-things-hard/"&gt;Naming things is hard&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;cifs&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The protocol being used for the share. I&amp;rsquo;ve got this shared folder set up as SMB, so I use CIFS. Some of my shares are NFS, so you could have &lt;code&gt;nfs&lt;/code&gt; at this position in the entry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;username=abs_user,password=SeCrErpaSSword&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It seems bad to have these credentials in /etc/fstab where any user on this system can read them, but I am the only user on this system and I don&amp;rsquo;t know what other convenient way I could get around this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;file_mode=0660&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read/write for user and group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;dir_mode=07&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read/write/execute on directories for user &amp;amp; group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s in the &lt;code&gt;/etc/fstab&lt;/code&gt;, you need to mount it with a &lt;code&gt;mount -a&lt;/code&gt;, then you should see the share by &lt;code&gt;ls&lt;/code&gt;-ing the mount point.&lt;/p&gt;
&lt;h4 id="docker-compose"&gt;Docker compose&lt;/h4&gt;
&lt;p&gt;Obviously this will vary with whatever service you&amp;rsquo;re running. Here&amp;rsquo;s mine for audiobookshare.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3&amp;#39;

services:
 audiobookshelf:
 image: ghcr.io/advplyr/audiobookshelf
 container_name: audiobookshelf
 ports:
 - &amp;#34;80:80&amp;#34;
 volumes:
 - ./config:/config
 - ./metadata:/metadata
 - /mnt/media:/audiobooks
 restart: always
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The notable things here are that I store all the container data - in this case &lt;code&gt;/config&lt;/code&gt; and &lt;code&gt;/metadata&lt;/code&gt; in subdirectories from the current directory, which is actually the user&amp;rsquo;s home directory. This LXC container is only for running this single service, so as soon as I &lt;code&gt;ssh&lt;/code&gt; in, everything I need to know or find out is easily discoverable, and easily accessible if I want to &lt;code&gt;scp&lt;/code&gt; it without a convoluted path.&lt;/p&gt;
&lt;p&gt;Another benefit of running in individual LXC&amp;rsquo;s is that each service has its own IP address - so I can use port 80 for every service.&lt;/p&gt;
&lt;h4 id="tailscale"&gt;Tailscale&lt;/h4&gt;
&lt;p&gt;Now that we can have up to 100 Tailscales on the free tier, every real service gets one. For the install, I just follow the &lt;a href="https://tailscale.com/kb/1174/install-debian-bookworm/"&gt;Debian Tailscale installation instructions&lt;/a&gt; since I&amp;rsquo;m using a Debian LXC. And now when we try &lt;code&gt;tailscale up&lt;/code&gt; we run into the LXC problem. I&amp;rsquo;ve already documented how to overcome that in &lt;a href="https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/"&gt;an earlier post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The combination of using Tailscale, and having access to port 80 means that the web address for this service will just be whatever hostname I gave it, in this case http://ct327-audiobookshelf&lt;/p&gt;
&lt;h4 id="ansible"&gt;Ansible&lt;/h4&gt;
&lt;p&gt;Some of the next steps are so common, I&amp;rsquo;ve set up Ansible playbooks for them, but to allow me to apply them to the new server, they need to be added into my Ansible infrastructure. First the hosts file where they get a host entry and some variables.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.48.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.48.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then in the encrypted &lt;code&gt;vault.yml&lt;/code&gt; file for the secrets. I&amp;rsquo;ve written about these before &lt;a href="https://blog.iankulin.com/first-ansible-playbook/"&gt;here&lt;/a&gt; and &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;here&lt;/a&gt;. Since I have &lt;code&gt;hosts:all&lt;/code&gt; in the playbook that runs all my &lt;a href="https://gist.github.com/IanKulin/41dbf097ac6bddd9e315859d3a06fe02"&gt;&lt;code&gt;apt&lt;/code&gt; updates&lt;/a&gt;, this now means the LXC container will get all it&amp;rsquo;s updates.&lt;/p&gt;
&lt;p&gt;Now we can automate some tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make this server use our &lt;code&gt;apt-cache&lt;/code&gt; server to make updates a bit faster and efficient. Described &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Install a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;little endpoint&lt;/a&gt; so the available memory and disk space can be monitored.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that endpoint is installed, I can add a couple of entries to my &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt; instance to keep track of the server health and notify me with &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;ntfy&lt;/a&gt; - so that&amp;rsquo;s monitoring covered off.&lt;/p&gt;
&lt;h4 id="backups"&gt;Backups&lt;/h4&gt;
&lt;p&gt;Backups in Proxmox are easy. I already have a general backup job set up for the prod DataCenter - it just snapshots every VM and LXC to the NAS at 1:00am each day. That&amp;rsquo;s plenty for this service - the only thing that would get lost would be a day&amp;rsquo;s worth of metadata, most of which is automatically pulled from web services anyway.&lt;/p&gt;
&lt;p&gt;This backup is of the LXC container with all the audiobookshelf config and code - not my book library. There is a backup process for it that&amp;rsquo;s a complicated collection of and external USB drive and &lt;code&gt;rsync&lt;/code&gt;-ing to a remote that might be a story for another day.&lt;/p&gt;
&lt;h3 id="done"&gt;Done&lt;/h3&gt;
&lt;p&gt;And that&amp;rsquo;s it. Now my audiobookshelf is running in an LXC container, serving the books off my NAS. The service is monitored for health, and there&amp;rsquo;s a backup plan in place. I can kick back and catch up on some technical reading.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_7018.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Ansible - Importing a Playbook</title><link>https://blog.iankulin.com/ansible-importing-a-playbook/</link><pubDate>Thu, 30 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ansible-importing-a-playbook/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/tags/ansible/"&gt;Ansible&lt;/a&gt; is a system for automating server tasks, and these tasks are written in a special yaml file called a playbook. I had need to call one playbook from another today and learned a couple of things.&lt;/p&gt;
&lt;h3 id="plays-vs-tasks"&gt;Plays vs Tasks&lt;/h3&gt;
&lt;p&gt;In Ansible we run &lt;em&gt;tasks&lt;/em&gt;. A group of tasks run against one particular sets of hosts is called a &lt;em&gt;play&lt;/em&gt;. Here is a playbook with one play, and two tasks:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Play 1 - Print the Book 1 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 1, Task 1

 - name: Print message 2
 debug:
 msg: Book 1, Task 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s possible to have multiple plays in a single &lt;em&gt;playbook&lt;/em&gt;. This would often be done if there was different tasks to run on different servers. For example you might want to run log rotations on you web servers, but reindexing on the database servers.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Play 1 - Print the Book 1 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 1, Task 1

 - name: Print message 2
 debug:
 msg: Book 1, Task 2

- name: Play 2
 hosts: 127.0.0.1

 tasks:
 - name: Print message
 debug:
 msg: This is the second play in Book 1
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="importing-playbooks"&gt;Importing Playbooks&lt;/h3&gt;
&lt;p&gt;When you run a playbook, generally the whole playbook gets run. So if we did have a playbook that included the tasks for the web servers and database servers, they would all be executed. That makes sense a lot of the time, but what if you usually wanted to run them together, but then sometimes just the database tasks?&lt;/p&gt;
&lt;p&gt;Ansible has an answer for this - you put the plays in different playbooks, but then import one into the top of the other one.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start over. Say this is our first playbook - &lt;code&gt;book1.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Play 1 - Print the Book 1 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message
 debug:
 msg: Book 1, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we run that with &lt;code&gt;ansible-playbook book1.yml&lt;/code&gt; the output will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PLAY [Play 1 - Print the Book 1 messages] *************************************************************************

TASK [Gathering Facts] *******************************************************************************
ok: [127.0.0.1]

TASK [Print message] *******************************************************************************
ok: [127.0.0.1] =&amp;gt; {
 &amp;#34;msg&amp;#34;: &amp;#34;Book 1, Task 1&amp;#34;
}

PLAY RECAP *******************************************************************************
127.0.0.1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we&amp;rsquo;ll make a &lt;code&gt;book2,yml&lt;/code&gt; very similar:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message
 debug:
 msg: Book 2, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can probably imagine the output from the example above - it&amp;rsquo;s identical except for the numbers in the messages.&lt;/p&gt;
&lt;p&gt;These two playbooks can easily be run separately, but then if we wanted to run them together sometimes, we could just make a new playbook that imported both of them:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- import_playbook: book1.yml
- import_playbook: book2.yml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we run this playbook, the output is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PLAY [Play 1 - Print the Book 1 messages] 
TASK [Gathering Facts] *******************************************************************************ok: [127.0.0.1]TASK [Print message] *******************************************************************************ok: [127.0.0.1] =&amp;gt; { &amp;#34;msg&amp;#34;: &amp;#34;Book 1, Task 1&amp;#34;}PLAY [Print the Book 2 messages] *******************************************************************************TASK [Gathering Facts] *******************************************************************************ok: [127.0.0.1]TASK [Print message 1] *******************************************************************************ok: [127.0.0.1] =&amp;gt; { &amp;#34;msg&amp;#34;: &amp;#34;Book 2, Task 1&amp;#34;}PLAY RECAP *******************************************************************************127.0.0.1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="potential-problems"&gt;Potential problems&lt;/h3&gt;
&lt;p&gt;Instead of making a new file to import both playbooks, you might want to just import one playbook into the other. For instance, you could achieve the same as we&amp;rsquo;ve done above by editing &lt;code&gt;book2.yml&lt;/code&gt; to import &lt;code&gt;book1.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- import_playbook: book1.yml
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 2, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s important to note that this has to be done at that level in the hierarchy. You can&amp;rsquo;t import a playbook in the middle of a play. For example this is legal yaml, but won&amp;rsquo;t work.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 import_playbook: book1.yml

 tasks:
 - name: Print message 1
 debug:
 msg: Book 2, Task 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Instead, we&amp;rsquo;ll get an message like &lt;code&gt;ERROR! 'hosts' is not a valid attribute for a PlaybookInclude&lt;/code&gt;. However this is fine:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Print the Book 2 messages
 hosts: 127.0.0.1

 tasks:
 - name: Print message 1
 debug:
 msg: Book 2, Task 1

- import_playbook: book1.yml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The import above is at the highest level of yaml, not inside the play, so it works well.&lt;/p&gt;</description></item><item><title>Building Docker images for multiple architectures</title><link>https://blog.iankulin.com/building-docker-images-for-multiple-architectures/</link><pubDate>Mon, 20 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/building-docker-images-for-multiple-architectures/</guid><description>&lt;p&gt;My little mdserver app has been a good way for me to start experimenting with the the devops side of things, especially building for Docker. Since I wanted to make the Docker image available for ARM Linux &amp;amp; x86 Linux I had a janky shell script that looked like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash

# Extract the version number from package.json using jq
VERSION=$(jq -r .version package.json)

docker build --platform linux/amd64 -t iankulin/mdserver:$VERSION -t iankulin/mdserver:latest .
docker build --platform linux/arm64 -t iankulin/mdserver:arm64-$VERSION -t iankulin/mdserver:arm64-latest .

docker push iankulin/mdserver:arm64-$VERSION 
docker push iankulin/mdserver:arm64-latest 

docker push iankulin/mdserver:$VERSION
docker push iankulin/mdserver:latest 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I&amp;rsquo;d build two different versions, and use the tags to separate them. In the registry it&amp;rsquo;d look like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.36.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.36.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But the big official images, for instance Node, have a long list of architectures associated with each tag - these are &lt;a href="https://docs.docker.com/build/building/multi-platform/"&gt;multi-platform images&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To create these, we need to use the &lt;code&gt;docker buildx&lt;/code&gt; feature. If you google how to do this, there&amp;rsquo;s a few mentions of how to &amp;rsquo;turn on&amp;rsquo; this &amp;rsquo;experimental&amp;rsquo; feature. I didn&amp;rsquo;t do that, so perhaps it&amp;rsquo;s been mainstreamed now. What I did to enable it was to enter:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker buildx create --use
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then to create my dual architecture image:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker buildx build --push \
--platform linux/arm64,linux/amd64 \
-t iankulin/mdserver:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then 113 seconds later (thank you Apple silicon), this showed up in my Docker Hub:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.47.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-29-at-3.47.50-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lovely!&lt;/p&gt;
&lt;p&gt;Buoyed by success, I decided I should also be shipping a Raspberry Pi version, which I guess is 32 bit ARM? So I dropped this into the CLI:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker buildx build --push \
--platform linux/arm64,linux/amd64,linux/arm/v7 \
-t iankulin/mdserver:latest .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This was somewhat less successful. I think the story of the building for other architectures with &lt;code&gt;buildx&lt;/code&gt; is that the container has QEMU binaries for them - ie like it&amp;rsquo;s running a little VM to do the build inside of. That&amp;rsquo;s three inception layers in, so I guess that&amp;rsquo;s why it&amp;rsquo;s slow for an alien architecture.&lt;/p&gt;
&lt;p&gt;In any case, this may still work, but at the time of writing, the &lt;code&gt;NPM install&lt;/code&gt; had been running overnight . If this speed turns out to be typical, it&amp;rsquo;s a good reason to look at outsourcing your Docker builds to GitHub actions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-30-at-7.03.18-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;With all eight cores pegged at 100% on an M1 MacBook:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-30-at-7.05.19-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-30-at-7.05.19-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Docker volume backup is more complicated than it should be</title><link>https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/</link><pubDate>Fri, 17 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/</guid><description>&lt;p&gt;&lt;a href="https://unccelearn.org/course/view.php?id=128&amp;page=overview&amp;lang=en"&gt;&lt;img src="https://blog.iankulin.com/images/big.jpg" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I set up my first Docker container (I think for &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;), I had read around and understood there were two choices for persistent; &lt;em&gt;bind mounts&lt;/em&gt; (where the data inside the container is effectively a symlink to a location on the local file system) or &lt;em&gt;name volumes&lt;/em&gt; where Docker abstracted that away a bit, so you didn&amp;rsquo;t have to worry where it was - I sort of understood Docker &amp;lsquo;managed&amp;rsquo; it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been lazily doing my &amp;lsquo;backups&amp;rsquo; by just saving snapshots of entire VM&amp;rsquo;s - which works really well, Proxmox handles the scheduling of them, I regularly test them (every month I run off the backup production server for a couple of days from the backups). I don&amp;rsquo;t mind that backing up up an entire VM for a couple of Dockerised apps is expensive in disk because local disk is cheap and it&amp;rsquo;s super convenient.&lt;/p&gt;
&lt;p&gt;However, I&amp;rsquo;ve got a couple of projects on the list where I&amp;rsquo;d like to move a container and it&amp;rsquo;s data between VM&amp;rsquo;s. One is trying out Jellyfin in Docker in an LXC, and another is moving the containers on my general utility dockerhost to a new VM with a bit larger disk since that seems easier than expanding the disk.&lt;/p&gt;
&lt;p&gt;I assumed I&amp;rsquo;d be stoping the container and doing something like &lt;code&gt;docker export portainer_data somebackupfile.name&lt;/code&gt; then moving that file over to the new system and running &lt;code&gt;docker import portainer_data somebackupfile.name&lt;/code&gt; to re-create it.&lt;/p&gt;
&lt;p&gt;But no, that&amp;rsquo;s not how it works. According to the Docker people, I need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use inspect to find out the internal data directories of the container&lt;/li&gt;
&lt;li&gt;Stop the container&lt;/li&gt;
&lt;li&gt;Create a new generic linux container&lt;/li&gt;
&lt;li&gt;Have it mount the docker volumes&lt;/li&gt;
&lt;li&gt;Also have it bind mount to the current directory&lt;/li&gt;
&lt;li&gt;Run a command inside the container to tar ball the internal data directory and save it to the bind mount&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The only real concession to usability along the way is that there&amp;rsquo;s a &lt;code&gt;--volumes_from&lt;/code&gt; flag that saves you from extracting all the volume names from a &lt;code&gt;docker inspect&lt;/code&gt; of the container whose data you want to back up.&lt;/p&gt;
&lt;h3 id="example"&gt;Example&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s run through those steps with an example. I&amp;rsquo;m going to set up &lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt; in Docker. I&amp;rsquo;ll use the &lt;a href="https://github.com/louislam/uptime-kuma/blob/master/docker/docker-compose.yml"&gt;suggested compose file&lt;/a&gt; which creates a named volume &lt;code&gt;uptime-kuma&lt;/code&gt;. I tested that&amp;rsquo;s up and running on port 3001 - when I visited there, it wanted me to create an admin account.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-9.55.47-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-9.55.47-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For demo purposes, I created the admin user &lt;code&gt;ian&lt;/code&gt; and set up Uptime Kuma to monitor Google for us.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.39.40-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.39.40-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you started the app from a docker compose file, you can just look in there to see what the internal data directories that are being mounted to are:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#39;3.8&amp;#39;

services:
 uptime-kuma:
 image: louislam/uptime-kuma:1
 container_name: uptime-kuma
 volumes:
 - uptime-kuma:/app/data
 ports:
 - &amp;#34;3001:3001&amp;#34; # &amp;lt;Host Port&amp;gt;:&amp;lt;Container Port&amp;gt;
 restart: always

volumes:
 uptime-kuma:
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;or alternatively use the &lt;code&gt;docker inspect &amp;lt;container name&amp;gt;&lt;/code&gt; command. You&amp;rsquo;ll get back a barrage of Json - somewhere in there will be the mount details:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;Mounts&amp;#34;: [
 {
 &amp;#34;Type&amp;#34;: &amp;#34;volume&amp;#34;,
 &amp;#34;Name&amp;#34;: &amp;#34;uptimekuma_uptime-kuma&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/var/lib/docker/volumes/uptimekuma_uptime-kuma/_data&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/app/data&amp;#34;,
 &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;z&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;&amp;#34;
 }
],
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Either way, we now know that the internal directory for data is &lt;code&gt;/app/data&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next stop the container with &lt;code&gt;docker stop uptime-kuma&lt;/code&gt;, then type in this bad boy based on the one in the docs.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /app/data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The highlighted bits are the pieces I changed for our demo - the name of our container and the internal data directory for it that we found in the steps above. Pulling down an entire &lt;a href="https://hub.docker.com/_/ubuntu"&gt;Ubuntu container&lt;/a&gt; seemed overkill - we&amp;rsquo;re just running a tar command so perhaps &lt;a href="https://hub.docker.com/_/alpine"&gt;Alpine&lt;/a&gt; or &lt;a href="https://hub.docker.com/_/busybox"&gt;Busybox&lt;/a&gt; would be fine, however, it pulled down quite quickly so it&amp;rsquo;s either smaller that I imagined or I already had the main layers locally.&lt;/p&gt;
&lt;p&gt;Now if we look in the directory where we ran that command, there should be a &lt;code&gt;backup.tar&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.34.38-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.34.38-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, for the purposes of this demo, I&amp;rsquo;ll copy the backup.tar (and my compose file) over to another VM and we&amp;rsquo;ll see if we can recreate this install.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d copied them over and &lt;a href="https://docs.docker.com/engine/install/debian/"&gt;installed Docker&lt;/a&gt;, I ran &lt;code&gt;docker compose up&lt;/code&gt; to start a new, empty Uptime Kuma. As expected, when I tried to visit the main page, it wanted me to create an admin user. Then I stopped the container. Note that you don&amp;rsquo;t want to &lt;code&gt;docker compose down&lt;/code&gt; to stop the container since that also removed it. If it&amp;rsquo;s removed, the next command won&amp;rsquo;t be able to find the name volumes it uses.&lt;/p&gt;
&lt;p&gt;Now we need copy the backed up data (which is just sitting in the current directory) into the named volume. Once again, this will be achieved by creating a new container, mounting the named volume and and current external working directory.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu bash -c &amp;#34;cd /app &amp;amp;&amp;amp; tar xvf /backup/backup.tar --strip 1&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once again, I&amp;rsquo;ve highlighted the bits I&amp;rsquo;ve changed from the &lt;a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"&gt;instructions&lt;/a&gt;. It&amp;rsquo;s important to note I&amp;rsquo;ve changed the destination directory. We backed up from &lt;code&gt;/app/data&lt;/code&gt; but we&amp;rsquo;re just restoring to &lt;code&gt;/app&lt;/code&gt; - the un-taring will copy the backed up data into the existing data directory. That&amp;rsquo;s a trick for young players - when I blindly followed the official instructions, I ended up with an &lt;code&gt;/app/data/data&lt;/code&gt; directory with the backed info which was, or course, ignored, and only discoverable buy &lt;code&gt;exec&lt;/code&gt;-ing into the container to see what was happening.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-11.34.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-11.34.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="why-not-just-copy-the-local-file-system-version"&gt;Why not just copy the local file system version?&lt;/h3&gt;
&lt;p&gt;The named docker volume is just stored on our local file system, usually at &lt;code&gt;/var/lib/docker/volumes&lt;/code&gt; so it would be reasonable to wonder why we don&amp;rsquo;t just copy that. I don&amp;rsquo;t have a great explanation for why not. I assume since the &lt;a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"&gt;official docs&lt;/a&gt; suggest something different and more complex that there must be a reason. Possibly there&amp;rsquo;s some extra Docker magic (file locks, caching, etc) going on we don&amp;rsquo;t know about, or there&amp;rsquo;s some planned for the future.&lt;/p&gt;</description></item><item><title>Ansible playbook to start Proxmox hosts</title><link>https://blog.iankulin.com/ansible-playbook-to-start-proxmox-hosts/</link><pubDate>Sun, 05 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ansible-playbook-to-start-proxmox-hosts/</guid><description>&lt;img src="https://blog.iankulin.com/images/mick-jagger-start-me-up-video-the-rolling-stones-far-out-magazine-copy.jpg" width="683" alt=""&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/proxmox-tags-to-solve-a-problem/"&gt;In my last post&lt;/a&gt;, I talked about tagging guests in a Proxmox node so I could easily see which VMs and LXCs I needed to manually start before I ran an Ansible script to run all my &lt;code&gt;apt updates&lt;/code&gt;. It would have been reasonable to wonder why I didn&amp;rsquo;t just add things to my playbook to magically do that.&lt;/p&gt;
&lt;p&gt;The answer would be, I haven&amp;rsquo;t gotten around to it yet, so here goes:&lt;/p&gt;
&lt;h3 id="modules"&gt;Modules&lt;/h3&gt;
&lt;p&gt;You might remember we discussed that the various functionalities for Ansible are in &lt;em&gt;modules&lt;/em&gt;. The modules for starting Proxmox guests are &lt;code&gt;[community.general.proxmox_kvm](https://docs.ansible.com/ansible/2.9/modules/proxmox_kvm_module.html)&lt;/code&gt; for VMs, and &lt;code&gt;[community.general.proxmox](https://docs.ansible.com/ansible/2.9/modules/proxmox_module.html)&lt;/code&gt; for LXC containers. If you look at the documentation for either of those, you&amp;rsquo;ll see a couple of prerequisites: &lt;em&gt;proxmoxer&lt;/em&gt; and &lt;em&gt;requests&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-8.18.46-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-8.18.46-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;requests&lt;/em&gt; is a common Python library (Ansible is actually running Python on the machines it&amp;rsquo;s configuring) for HTTP requests. We can ignore it since (a) you probably already have it installed, and (b) if not, when we install &lt;em&gt;proxmoxer&lt;/em&gt;, it will be installed as a dependency. You&amp;rsquo;ve probably already guessed that &lt;em&gt;proxmoxer&lt;/em&gt; is the Python library for interacting with Proxmox through it&amp;rsquo;s API.&lt;/p&gt;
&lt;p&gt;So before we can start any of the guests, we need to ensure proxmoxer is installed:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; tasks:

 - name: Install proxmoxer
 apt:
 name: python3-proxmoxer
 state: latest
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="my-ansible-setup"&gt;My Ansible setup&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s probably worth going over how my Ansible is set up so you can make sense of the rest of this without going back to read earlier posts. In the directory where I&amp;rsquo;m running this playbook, I have an &lt;code&gt;ansible.cfg&lt;/code&gt; file. Here&amp;rsquo;s the entire contents:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[defaults]
INVENTORY = hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It&amp;rsquo;s an INI type file, and in this case it&amp;rsquo;s just saying if I don&amp;rsquo;t specify the name of an inventory file (a list of all my machines and their IP addresses or names), then use the file named &amp;lsquo;hosts&amp;rsquo;. This just saves me specifying the inventory file at the command line with the flag &lt;code&gt;-i&lt;/code&gt; each time.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;hosts&lt;/code&gt; file looks a bit like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[pve_dev1]
pve-dev1
#192.168.100.28

[pve_dev1:vars]
ansible_user=&amp;#39;{{pve_dev1_user}}&amp;#39;
ansible_become_password=&amp;#39;{{pve_dev1_become_pass}}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s a couple of these entries for every &amp;lsquo;machine&amp;rsquo; that I manage. The first bit just gives the address for the machine, and the second the variables for that machine - a sudo user and their password. You could just type those entries in here like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[pve_dev1]
pve-dev1
#192.168.100.28

[pve_dev1:vars]
ansible_user=root
ansible_become_password=password1234
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Instead of putting my credentials in a text file that&amp;rsquo;s pushed up to github, I use another file called a &amp;lsquo;vault&amp;rsquo; which is encrypted to keep them in. I&amp;rsquo;ve explained about &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;that elsewhere,&lt;/a&gt; but to understand what&amp;rsquo;s going on here, you just need to know that &lt;code&gt;'{{pve_dev1_user}}'&lt;/code&gt; gets resolved to &lt;code&gt;root&lt;/code&gt; when the playbook is run.&lt;/p&gt;
&lt;p&gt;You might also be wondering about the IP address that&amp;rsquo;s commented out in the snippets above. I am using the Tailscale MagicDNS on my machines, so I can just refer to this dev Proxmox instance as &lt;code&gt;pve-dev1&lt;/code&gt;, but yours is probably setup with IP address instead- in which case use that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[pve_dev1]
192.168.100.28

[pve_dev1:vars]
ansible_user=root
ansible_become_password=password1234
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So now the name being used in Ansible is pve_dev1, but it&amp;rsquo;s referring to the machine at 192.168.100.28&lt;/p&gt;
&lt;h3 id="starting-a-proxmox-vm"&gt;Starting a Proxmox VM&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - name: start vm321-deb
 community.general.proxmox_kvm:
 api_user : root@pam
 api_password: &amp;#39;{{pve_dev1_become_pass}}&amp;#39;
 api_host : pve-dev1
 name : vm321-deb
 state : started
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The api_host is the address of the node, and the user and password above it are the same ones you use to log into the web gui of this Proxmox server. name is the you gave the VM in Proxmox when you created it. Note that this is for a stand-alone Proxmox server, not a node that&amp;rsquo;s part of a cluster. If we had a cluster called &amp;lsquo;mycluster&amp;rsquo; and the server/node that vm321-deb was hosted on was called &amp;rsquo;node2&amp;rsquo; the Ansible entry for it would be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - name: start vm321-deb
 community.general.proxmox_kvm:
 api_user : root@pam
 api_password: &amp;#39;{{pve_dev1_become_pass}}&amp;#39;
 api_host : mycluster
 node : node2
 name : vm321-deb
 state : started
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="starting-an-lxc-container"&gt;Starting an LXC container&lt;/h3&gt;
&lt;p&gt;Increasingly, I run services in their own LXC container. They are quick to create and start, use less resources, but can still be snapshot-ed for easy backups.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; - name: start ct351-go
 community.general.proxmox:
 api_user : root@pam
 api_password: &amp;#39;{{pve_dev1_become_pass}}&amp;#39;
 api_host : pve-dev1
 vmid : 351
 state : started
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So for these containers, we use a different module, and call them by their VMID instead of name.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the full playbook.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Start pve-dev hosts for updating&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ansible-playbook start-apt-dev-vms.yaml --ask-vault-pass &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vars_files&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./vault.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hosts&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;become&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tasks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Install proxmoxer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;apt&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python3-proxmoxer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start babybuntu&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;babybuntu&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start vm321-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vm321-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start vm322-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vm322-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start vm323-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox_kvm&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vm323-deb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start ct351-go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vmid &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;351&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start ct353-omada&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vmid &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;353&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;start ct356-proxy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;community.general.proxmox&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_user &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;root@pam&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_password&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;{{pve_dev1_become_pass}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;api_host &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pve-dev1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vmid &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;356&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;state &lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;started&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Proxmox tags to solve a problem</title><link>https://blog.iankulin.com/proxmox-tags-to-solve-a-problem/</link><pubDate>Thu, 02 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-tags-to-solve-a-problem/</guid><description>&lt;p&gt;Each weekend I run an Ansible script that updates all my apt based VMs and containers. For the production machines, that&amp;rsquo;s everything, but my dev Proxmox is full of half-finished projects. Some of these have IP addresses reserved and are in the Ansible hosts file (because whatever service they are running is almost ready to move to the production server) others do not.&lt;/p&gt;
&lt;p&gt;Long story short, the dev server has some containers and VM&amp;rsquo;s that need turned on before I run the updates, and some that don&amp;rsquo;t. I could just start them all up, for the ten minutes the updates usually take, but that seems wasteful somehow. If there was only some way to mark the ones I need to turn on in the Proxmox webgui! Well, there is. We can add tags to machines in Proxmox.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-11.23.57-am-copy.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-11.23.57-am-copy.png" width="512" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Proxmox has quite a &lt;a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_tags"&gt;comprehensive tagging system&lt;/a&gt; - there are different display formats, and tags can be limited to a specific set, or completely free form. Also, there&amp;rsquo;s a heap of command line tools to work with them. For this job, I don&amp;rsquo;t really need much of that stuff - I just want to click a few things in the web gui to mark some of my VM&amp;rsquo;s with a coloured marker so I know which ones to start when I&amp;rsquo;m going to run my updates.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the steps.&lt;/p&gt;
&lt;p&gt;Go into &lt;code&gt;DataCenter | Options&lt;/code&gt;. One of the options is &lt;code&gt;Tag Style Override&lt;/code&gt;. It&amp;rsquo;s called &amp;ldquo;Override&amp;rdquo; because by default, the colours are deterministically figured out from the tag text. I want to just have a nice dark blue associated with the tag &lt;code&gt;apt&lt;/code&gt;, so I&amp;rsquo;m going to set it. It turns out I could have just skipped this step and got a nice light blue for &lt;code&gt;apt&lt;/code&gt;. This system (of just figuring out a colour from the text) means in most cases you can completely skip this step. Each machine you tag with a particular tag will be marked with the same colour - it will just work. &lt;code&gt;test&lt;/code&gt; = pink, &lt;code&gt;fred&lt;/code&gt; = green, and so on.&lt;/p&gt;
&lt;p&gt;Back to me being fussy. Opening up the &lt;code&gt;Tag Style Override&lt;/code&gt; I&amp;rsquo;m setting apt to be dark blue with white text.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-2.58.00-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-2.58.00-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To apply these tags, you just click on the machine you want to tag, then notice that up the top of the web gui, next to the machine name, it says &amp;ldquo;No Tags&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.07.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.07.11-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You just click on the pencil, and enter the tag name. If you haven&amp;rsquo;t changed any of the other defaults, a coloured circle will appear next to the machine in the server view.&lt;/p&gt;
&lt;p&gt;There are three display options for the tags - &amp;ldquo;full&amp;rdquo; which is a coloured bar including the text of the tag, &amp;ldquo;circle&amp;rdquo; which is the one shown in the first screenshot above, and &amp;ldquo;dense&amp;rdquo; which is a small rectangular bar - designed for stacking several different tags against each machine. All these options are under &amp;ldquo;tree shape&amp;rdquo; in the &lt;code&gt;Tag Color Override&lt;/code&gt; dialogue we opened earlier.&lt;/p&gt;
&lt;p&gt;As well as being able to see the tag blobs in the tree view, if you look at all your machines on the &lt;code&gt;Datacenter | Search&lt;/code&gt; view, it&amp;rsquo;s possible to sort by tags - which will even further simplify the job for me of starting them all up before I run the updates.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.35.21-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-14-at-3.35.21-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>apt update - BADSIG 871920D1991BC93C</title><link>https://blog.iankulin.com/apt-update-badsig-871920d1991bc93c/</link><pubDate>Mon, 30 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/apt-update-badsig-871920d1991bc93c/</guid><description>&lt;p&gt;I have an ansible script that runs each weekend which basically does an &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -Y&lt;/code&gt; on every Debian based instance. This weekend it failed on one Ubuntu host. When I went it to try it manually, this was the output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Hit:1 http://au.archive.ubuntu.com/ubuntu jammy InRelease
Hit:2 https://download.docker.com/linux/ubuntu jammy InRelease 
Hit:3 http://au.archive.ubuntu.com/ubuntu jammy-backports InRelease 
Hit:4 http://au.archive.ubuntu.com/ubuntu jammy-security InRelease 
Get:5 http://au.archive.ubuntu.com/ubuntu jammy-updates InRelease [119 kB] 
Err:5 http://au.archive.ubuntu.com/ubuntu jammy-updates InRelease 
 The following signatures were invalid: BADSIG 871920D1991BC93C Ubuntu Archive Automatic Signing Key (2018) &amp;lt;ftpmaster@ubuntu.com&amp;gt;
Get:6 https://pkgs.tailscale.com/stable/ubuntu jammy InRelease
Fetched 125 kB in 1s (125 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
11 packages can be upgraded. Run &amp;#39;apt list --upgradable&amp;#39; to see them.
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://au.archive.ubuntu.com/ubuntu jammy-updates InRelease: The following signatures were invalid: BADSIG 871920D1991BC93C Ubuntu Archive Automatic Signing Key (2018) &amp;lt;ftpmaster@ubuntu.com&amp;gt;
W: Failed to fetch http://au.archive.ubuntu.com/ubuntu/dists/jammy-updates/InRelease The following signatures were invalid: BADSIG 871920D1991BC93C Ubuntu Archive Automatic Signing Key (2018) &amp;lt;ftpmaster@ubuntu.com&amp;gt;
W: Some index files failed to download. They have been ignored, or old ones used instead.
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="solved"&gt;Solved&lt;/h3&gt;
&lt;p&gt;The first &lt;a href="https://ubuntuforums.org/showthread.php?t=2484710"&gt;google result&lt;/a&gt; mentions apt-cache - which &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;I also run&lt;/a&gt;, so a first level debug step is to delete the &lt;code&gt;/etc/apt/apt.conf.d/00aptproxy&lt;/code&gt; file that redirects apt requests to the cache I run in an LXC container. After that, if I re-run the &lt;code&gt;apt update&lt;/code&gt; it works perfectly. Seems like a problem with the cache then. I&amp;rsquo;m not sure why it would only affect this host though - I have other Ubuntu VM&amp;rsquo;s in the fleet that are not getting the original error.&lt;/p&gt;
&lt;p&gt;In any case, adding the conf back to force the server to use the cache made the error reappear - so it&amp;rsquo;s definitely related to the cache. With any type of cache, when there&amp;rsquo;s a problem related to it, deleting the contents is usually a &amp;ldquo;plan A&amp;rdquo; response. Assuming there&amp;rsquo;s some mechanism in &lt;a href="https://wiki.debian.org/AptCacherNg"&gt;Apt Cacher NG&lt;/a&gt; to do this, I went to the little stats/config webpage it serves up.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-08-at-8.41.44-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well &amp;ldquo;Force the download of index files&amp;rdquo; sounds promising, let&amp;rsquo;s try that.&lt;/p&gt;
&lt;p&gt;I ticked the box for Force the download of index files (even having fresh ones), but it wasn&amp;rsquo;t clear to me how to make that change stick. The first button I could click further down the page was &amp;ldquo;Start Scan&amp;rdquo; which was related to some different checkboxes. I tried it anyway, but it didn&amp;rsquo;t force the downloading of index files. Time for some command line comandoing.&lt;/p&gt;
&lt;p&gt;The cache files for &lt;code&gt;aptcacher-ng&lt;/code&gt; are in &lt;code&gt;/var/cache/apt-cacher-ng/&lt;/code&gt; each distro has a directory in there.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-08-at-9.08.48-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Guessing the Ubuntu repository cache is probably stored in &lt;code&gt;uburep&lt;/code&gt;, I deleted that with &lt;code&gt;rm -R /var/cache/apt-cacher-ng/uburep&lt;/code&gt;. When I retried the &lt;code&gt;apt update&lt;/code&gt;, it worked perfectly, and I could see that the &lt;code&gt;/var/cache/apt-cacher-ng/uburep&lt;/code&gt; directory had re-appeared.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the immediate problem fixed. The cause of this problem is unclear. Presumably it related to a package running on this Ubuntu machine (runs docker with a couple of small services) that is not running on my other Ubuntu hosts. It probably falls into the category of &amp;ldquo;don&amp;rsquo;t worry about unless it crops up again&amp;rdquo;.&lt;/p&gt;</description></item><item><title>Certbot - adding more virtual hosts</title><link>https://blog.iankulin.com/certbot-adding-more-virtual-hosts/</link><pubDate>Sun, 15 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/certbot-adding-more-virtual-hosts/</guid><description>&lt;p&gt;I&amp;rsquo;ve got a domain that&amp;rsquo;s not currently used, so I&amp;rsquo;m going to set it up as a virtual host under NGINX. This server is already serving two domains set up with Certbot for SSL. Is it going to be possible to add another site and have Certbot manage the certificates for it after I&amp;rsquo;ve run Certbot once?&lt;/p&gt;
&lt;p&gt;When I googled around to find out, I didn&amp;rsquo;t find anything - which is usually a sign I&amp;rsquo;m either asking a wrong question, or it&amp;rsquo;s so little drama that no one ever mentions it. I decided just to move the site, check it was all working for the http version, then run Certbot and see what it said.&lt;/p&gt;
&lt;p&gt;Since I already had Certbot installed, I just ran &lt;code&gt;sudo certbot --nginx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-09-03-at-10.03.19-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-03-at-10.03.19-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s probably worth explaining at this point that Certbot does not obtain separate certificates for each domain (which is what I&amp;rsquo;d been doing when I was doing this manually), but instead grabs a single certificate that includes all the domains, and stores it under the the first domain - in the case above, for agnet.&lt;/p&gt;
&lt;p&gt;I hit &amp;ldquo;E&amp;rdquo; for Expand, and Certbot did it&amp;rsquo;s thing by acquiring the new certificate expanded to cover the new domain and installed it. No drama.&lt;/p&gt;
&lt;h3 id="what-if-you-already-have-a-certificate-from-another-provider"&gt;What if you already have a certificate from another provider?&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve got two more domains to move from another server, but both of these already have active SSL certificates that I obtained via Porkbun. Is that going to be a problem? Can Let&amp;rsquo;s Encrypt (who actually does the certificates for Porkbun) include these sites on the combined certificate on my main VPS so I can use Certbot to maintain them? Let&amp;rsquo;s see.&lt;/p&gt;
&lt;p&gt;I went through the same routine - created a nginx conf for the virtual host in &lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;, created a simple index.html in &lt;code&gt;/var/www/drysea.xyz&lt;/code&gt; and then symlinked the conf file into &lt;code&gt;/etc/nginx/sites-enabled&lt;/code&gt;. Then changed the A records for the DNS to point to the server address and waited for them to propagate so I could test the http version of the site.&lt;/p&gt;
&lt;p&gt;After that, I ran the sudo certbot &amp;ndash;nginx command again, and exactly as before, it asked if I wanted to expand the existing certificate. I did that, and the site can now be visited securely with no warning about the incorrect certificate. So that&amp;rsquo;s all worked well.&lt;/p&gt;
&lt;p&gt;It is allowable for a site to have more than one active, valid SSL certificate. This often happens in the exact scenario we&amp;rsquo;ve got here where domains are being moved around. There is a security implication for this though. A &lt;a href="https://www.csoonline.com/article/561111/dns-record-will-help-prevent-unauthorized-ssl-certificates.html"&gt;system&lt;/a&gt; of entering a particular DNS record that would prevent certificates being issued by all but one particular certificate authority exists, but is not widely used.&lt;/p&gt;
&lt;p&gt;It is probably a good idea for my to change my configuration on Porkbun to stop it from going on generating certificates that are not needed though, so I&amp;rsquo;ll go ahead and revoke that.&lt;/p&gt;</description></item><item><title>Certbot &amp; Let's Encrypt are great</title><link>https://blog.iankulin.com/certbot-lets-encrypt-are-great/</link><pubDate>Thu, 12 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/certbot-lets-encrypt-are-great/</guid><description>&lt;img src="https://blog.iankulin.com/images/certbot.png" width="847" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been managing SSL certificates for my domains purchased from &lt;a href="https://porkbun.com/"&gt;PorkBun&lt;/a&gt; by going there every 90 days downloading the certificates, &lt;a href="https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/"&gt;joining them together&lt;/a&gt; to make the &lt;code&gt;fullchain.pem&lt;/code&gt; then &lt;code&gt;scp&lt;/code&gt;-ing them to my servers. That&amp;rsquo;s been sort of manageable, but less than ideal.&lt;/p&gt;
&lt;p&gt;It also doesn&amp;rsquo;t work for my Australian domains. Since there&amp;rsquo;s strict rules about who can own a domain in the &lt;code&gt;.au&lt;/code&gt; space (&lt;em&gt;you have to have some sort of right to the name - a random person can&amp;rsquo;t obtain the &lt;code&gt;coke.com.au&lt;/code&gt; domain unless that&amp;rsquo;s a trading name, a trademark, or something similar&lt;/em&gt;), they have to be managed by one of about eight organisations, and the offerings are much simpler.&lt;/p&gt;
&lt;p&gt;No problem though for two wonderful reasons - &lt;a href="https://letsencrypt.org/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; and &lt;a href="https://certbot.eff.org/"&gt;Certbot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s Encrypt is a free, automated, and open certificate authority (CA), run for the public’s benefit. It is a service provided by the Internet Security Research Group. They provide free TLS certificates to allow websites to use SSL.&lt;/p&gt;
&lt;p&gt;Certbot, managed by the Electronic Frontiers Foundation, is a utility to automatically obtain certificates for a website from Let&amp;rsquo;s Encrypt, and change the server configuration files to use them.&lt;/p&gt;
&lt;p&gt;This makes this whole process amazingly painless. There&amp;rsquo;s really no excuse for not adding this to your websites, and I&amp;rsquo;d highly encourage you to donate to both projects if you use Certbot.&lt;/p&gt;
&lt;h2 id="certbot"&gt;Certbot&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m running NGINX on Ubuntu LTS on my VPS&amp;rsquo;s, so installation was a snap (pun intended). I just followed the &lt;a href="https://certbot.eff.org/instructions?ws=nginx&amp;amp;os=ubuntufocal"&gt;instructions&lt;/a&gt; which involved installing the snap, adding a symlink to ensure it was in my path, then running the bot passing it a flag to say I was using NGINX.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-09-02-at-4.35.25-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-09-02-at-4.35.25-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It asks you a couple of questions, intelligently (by reading all the nginx conf files) then downloads the certificates and edits the nginx site conf files to use them. It also adds a systemd timer command to automate checking to see if they need renewed every couple of hours.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done, you just go back to your website and you&amp;rsquo;ve got the magical padlock, and won&amp;rsquo;t have to worry about it again due to the automatic renewal.&lt;/p&gt;</description></item><item><title>Solved DNS Issues - Proxmox, LXC, Ubuntu, Tailscale</title><link>https://blog.iankulin.com/solved-dns-issues-proxmox-lxc-ubuntu-tailscale/</link><pubDate>Fri, 06 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/solved-dns-issues-proxmox-lxc-ubuntu-tailscale/</guid><description>&lt;p&gt;&lt;a href="https://i.imgur.com/WmRbmf5.png"&gt;&lt;img src="https://blog.iankulin.com/images/wmrbmf5.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve picked up an new TP-Link WAP with Omada, so I wanted to spin up an Ubuntu 20.04 LXC to run the controller software in, and ended up spending a couple of hours figuring out why things where not working.&lt;/p&gt;
&lt;p&gt;The initial problem was I was having connectivity issues pulling down the updates for all the packages required. I went down a bit of a tangent because I installed an apt cache the other day, so I was looking for problems there. Eventually I narrowed it down to DNS not working and started A/B testing like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-26-at-4.49.24-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A more seasoned sysadmin probably would have been looking at the &lt;code&gt;/etc/resolv.conf&lt;/code&gt; a bit earlier where the glaring hint was. I&amp;rsquo;ll get to that in a second, but first a bit about my setup.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running Proxmox 8.0.4 on one of my HP G2 800 Minis (love these little power-frugal &lt;a href="https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/"&gt;gems&lt;/a&gt;) and I use Tailscale to tie all my network (my homelab here, and two remote locations) together. The Tailscale version on this node is 1.48.1&lt;/p&gt;
&lt;p&gt;You can see in the table above, that a LXC using the Ubuntu 20.04 template had no domain name resolution, but the Debian 12 (and Debian 11 I tried earlier did). The &lt;code&gt;/etc/resolv.conf&lt;/code&gt; on the Debian containers looked like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nameserver 192.168.100.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And on the Ubuntu container&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# --- BEGIN PVE ---
search tailaf96a.ts.net
nameserver 100.100.100.100
# --- END PVE ---
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;192.168.100.1&lt;/code&gt; is my local DNS which is provided from the DHCP, but clearly Ubuntu is not using that. The &lt;code&gt;PVE&lt;/code&gt; comments tells me it&amp;rsquo;s Proxmox messing with my container, and that&amp;rsquo;s the Tailscale DNS server number in there. The container does not have a route to &lt;code&gt;100.100.100.100&lt;/code&gt; so that DNS is not going to be able to resolved anything.&lt;/p&gt;
&lt;p&gt;So, that&amp;rsquo;s a bit weird, but easily fixed by just editing this back to set the nameserver to &lt;code&gt;192.160.100.1&lt;/code&gt; right? Well, yes - if you do that, it works, but then as soon as the container is rebooted, the Tailnet DNS gets written back in. Those blocky PVE comments are probably part of the automated system for doing that. So, what&amp;rsquo;s going on here?&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s two screens for network configuration when you&amp;rsquo;re creating an &lt;a href="https://en.wikipedia.org/wiki/RAS_syndrome"&gt;LXC container&lt;/a&gt; in the Proxmox GUI.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-26-at-4.55.54-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-26-at-4.56.03-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no option in the GUI to just say &lt;em&gt;&amp;ldquo;Use the DNS settings provided by the DHCP server&amp;rdquo;&lt;/em&gt;, although we&amp;rsquo;ll see later, there is a work around for this.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;d been leaving the &lt;code&gt;DNS domain:&lt;/code&gt; set to &lt;code&gt;use host settings&lt;/code&gt;. You might reasonably wonder what the Proxmox node /etc/resolv.conf looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# resolv.conf(5) file generated by tailscale
# For more info, see https://tailscale.com/s/resolvconf-overwrite
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN

nameserver 100.100.100.100
search tailaf96a.ts.net local
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So actually, although I was thinking there must be some bug with Ubuntu since Debian was working how I expected, it&amp;rsquo;s the other way around - Ubuntu and Proxmox are working together to do exactly what the settings have told it to - to use the host settings. And actually, the Debian containers are not working correctly (although they were working how I expected). The process of Proxmox making these types of changes is documented in the &lt;a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_guest_operating_system_configuration"&gt;Admin Guide&lt;/a&gt;. I&amp;rsquo;d actually never seen that guide till today (although there is a large &amp;ldquo;Documentation&amp;rdquo; button in the top right of the web GUI), but it looks pretty great so I&amp;rsquo;ll be revisiting it.&lt;/p&gt;
&lt;h3 id="solution-1"&gt;Solution 1&lt;/h3&gt;
&lt;p&gt;The first solution is just to specify the DNS address in the GUI - then our container works exactly as the PVE developers intended. A slight downside is that if I change the network configuration in future and update the DNS address in the DHCP server (which is the logical way to do that) then it won&amp;rsquo;t update for this container and domain name resolution will stop working for it.&lt;/p&gt;
&lt;p&gt;If I do that, the &lt;code&gt;/etc/resolv.conf&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# --- BEGIN PVE ---
search tailaf96a.ts.net
nameserver 192.168.100.1
# --- END PVE ---
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And it all works fine.&lt;/p&gt;
&lt;h3 id="solution-2"&gt;Solution 2&lt;/h3&gt;
&lt;p&gt;This &lt;a href="https://forum.proxmox.com/threads/lxc-dns-from-dhcp.36200/"&gt;post on the Proxmox Forums&lt;/a&gt; lead me to a second solution. It&amp;rsquo;s possible to stop Proxmox from adding the host by adding a little signal file with&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;touch /etc/.pve-ignore.resolv.conf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When Proxmox sees that. it won&amp;rsquo;t mess with the &lt;code&gt;/etc/resolv.conf&lt;/code&gt; file, so if that&amp;rsquo;s been edited to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nameserver 192.168.100.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It will be left alone, and things will work fine. This is not quite what I&amp;rsquo;d like - I&amp;rsquo;d really prefer it picks everything up from DHCP, but I don&amp;rsquo;t know enough about how that works in Linux to fix it, yet.&lt;/p&gt;</description></item><item><title>Caching APT updates</title><link>https://blog.iankulin.com/caching-apt-updates/</link><pubDate>Tue, 03 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/caching-apt-updates/</guid><description>&lt;p&gt;It&amp;rsquo;s bothered me for a while that all these VM&amp;rsquo;s are pulling down a lot of the same updates. As well as needlessly using some bandwidth, I&amp;rsquo;m hammering the update servers (that I don&amp;rsquo;t pay for) with the same requests over and over. I did briefly consider running my own mirror, but that&amp;rsquo;s not simple, plus I&amp;rsquo;d then be mirroring a heap of files in a complete repository that I&amp;rsquo;d never use. What I really needed was some sort of cache so once I&amp;rsquo;ll pulled down an update, it would hang around for a few days being available to other machines on the local network. Luckily, that exact thing exists - &lt;a href="https://www.unix-ag.uni-kl.de/~bloch/acng/html/index.html"&gt;APT Cacher NG&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It works pretty much as described above - all of the machines on the LAN have their APT calls proxied through a little server. If the server doesn&amp;rsquo;t have a copy of the appropriate package, it pulls it down and delivers it. If it&amp;rsquo;s got a good copy already, it just provides that.&lt;/p&gt;
&lt;h3 id="installing-the-server"&gt;Installing the server&lt;/h3&gt;
&lt;p&gt;I decided an unprivileged LXC container would be the perfect base for this service. I created one from the Debian 12 image with 1MB RAM but a largish 30GB drive. I don&amp;rsquo;t really have any feel for how big the cache will get under normal use so I erred on the large side. It&amp;rsquo;s not doing any computationally expensive work, so one CPU is plenty.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.42.06-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.42.06-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then we just install it, and start and enable it as a service.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt install apt-cacher-ng
systemctl start apt-cacher-ng
systemctl enable apt-cacher-ng
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;During the install, it asked me about https:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.46.32-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.46.32-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I said no, but then to enable it, I had to (after the installation) edit the config file at &lt;code&gt;/etc/apt-cacher-ng/acng.conf&lt;/code&gt; to uncomment the line&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PassThroughPattern: .* # this would allow CONNECT to everything&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With that done, and the service restarted, we&amp;rsquo;re now serving proxies at localhost:3142, and also a little web page with some advice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-2.37.46-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="installing-the-client"&gt;Installing the client&lt;/h3&gt;
&lt;p&gt;The two bits of information I&amp;rsquo;ve put red boxes around are the things we need to do to enable &lt;code&gt;apt&lt;/code&gt; on the client machines to use the cache. We need to create a file called &lt;code&gt;/etc/apt/apt.conf.d/00aptproxy&lt;/code&gt; and add the single line to it of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Acquire::http::Proxy &amp;quot;http://192.168.100.37:3142&amp;quot;;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Note that the ip address will be different on yours, just copy it off the little web page. Since I&amp;rsquo;ve got a heap of machines to do this do, I made the &lt;code&gt;conf&lt;/code&gt; file once and pushed out out with Ansible.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.00.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.00.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;hosts: local&lt;/code&gt; in the pkaybook refers to the &lt;code&gt;local: children&lt;/code&gt; group in my hosts ini file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.14.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="statistics"&gt;Statistics&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re curious to see what the savings are, there&amp;rsquo;s another web page served by the cache at &lt;code&gt;&amp;lt;server ip&amp;gt;:3142/acng-report.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.16.27-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Further down on that page are some options that can be changed as well.&lt;/p&gt;
&lt;h3 id="resources"&gt;Resources&lt;/h3&gt;
&lt;p&gt;I learned most of this from &lt;a href="https://www.youtube.com/watch?v=t8kI4YwdvRA"&gt;this video by RickMakes&lt;/a&gt;, and &lt;a href="https://www.linuxhelp.com/how-to-set-up-apt-caching-server-using-apt-cacher-ng-on-debian-11-3"&gt;this Linux Help page&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Installing service with Ansible</title><link>https://blog.iankulin.com/installing-service-with-ansible/</link><pubDate>Sat, 30 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-service-with-ansible/</guid><description>&lt;p&gt;Having written my little monitoring endpoint in Go, it needs pushed out to all my servers and VM&amp;rsquo;s. Clearly this is a job for Ansible which I&amp;rsquo;ve already &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;dabbled my toes in&lt;/a&gt;. Before we get onto doing that though, we need to have a think about how to make it a service.&lt;/p&gt;
&lt;h3 id="linux-services"&gt;Linux Services&lt;/h3&gt;
&lt;p&gt;A service in Linux is just a program, but one that&amp;rsquo;s usually required to be running all the time to provide some piece of functionality. The &amp;ldquo;program&amp;rdquo; can be any executable, but to allow systemd to manage it, we need to tell it a bit about what we want in a &lt;code&gt;.service&lt;/code&gt; file. This file is used by &lt;code&gt;systemd&lt;/code&gt; to know how to manage the service. They can get quite complex, but here&amp;rsquo;s the simple one for &lt;code&gt;vitals-glimpse&lt;/code&gt; - my little monitoring API endpoint.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-19-at-11.23.21-am.png" alt="[Unit]
Description=Memory and Disk statistics server on port 10321
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/vitals-glimpse
[Install]
WantedBy=default.target"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExecStart&lt;/code&gt; is just saying what executable file is to be run. In this case it&amp;rsquo;s my compiled Go program. It&amp;rsquo;s a whopping 6MB so I&amp;rsquo;m assuming it&amp;rsquo;s all statically linked and standalone, so to run it we just copy it into &lt;code&gt;/usr/local/bin&lt;/code&gt; and run it from there.&lt;/p&gt;
&lt;p&gt;The two lines mentioning &lt;code&gt;.target&lt;/code&gt;s might not be obvious. These refer to the different times things happen in the machine startup sequence. &lt;code&gt;After=network.target&lt;/code&gt; means &amp;ldquo;don&amp;rsquo;t start this until the network is up and running&amp;rdquo;. You can see how it would be pointless to start a server that&amp;rsquo;s listening on a network port before networking is live. &lt;code&gt;default.target&lt;/code&gt; is just the system state when everything is going and ready for the users to interact with things, so when we specify &lt;code&gt;WantedBy=default.target&lt;/code&gt; we&amp;rsquo;re just saying &amp;ldquo;this service needs to be running by the time we are ready for user interactions&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;I already have my hosts file listing every machine, and an encrypted vault for my secrets (we&amp;rsquo;ve discussed those before), so the installation Ansible playbook just needs to copy the executable file into place in &lt;code&gt;/usr/local/bin&lt;/code&gt;, mark it as executable, copy the service file into place, and then start the service.&lt;/p&gt;
&lt;p&gt;If the files are already up to date and we don&amp;rsquo;t copy anything, then there&amp;rsquo;s no need touch the service, but if we have copied a new file, then we want to restart the service to pick up the change. Here&amp;rsquo;s how that all looks.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
- name: Install vitals-glimpse to a Debian based server
 # ansible-playbook vg-install.yml --ask-vault-pass 
 vars_files: ./vault.yml
 hosts: vm100-dockhost
 become: true

 tasks:
 - name: Copy service file
 ansible.builtin.copy:
 src: files/vitals-glimpse.service
 dest: /etc/systemd/system/vitals-glimpse.service
 notify: Restart vitals-glimpse

 - name: Copy executable
 ansible.builtin.copy:
 src: files/vitals-glimpse
 dest: /usr/local/bin/vitals-glimpse
 mode: &amp;#39;0755&amp;#39; # Set the executable permissions
 notify: Restart vitals-glimpse

 handlers:
 - name: Restart vitals-glimpse
 ansible.builtin.service:
 name: vitals-glimpse
 state: restarted
 enabled: yes
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing to know is that I have a hosts inventory file in my Ansible config, and &lt;code&gt;vm100-dockhost&lt;/code&gt; is just one of those hosts. The sudo credentials for that host are in the &lt;code&gt;vault.yml&lt;/code&gt; file mentioned in the code as &lt;code&gt;vars_file&lt;/code&gt;. I&amp;rsquo;ve started putting the command I need to run each playbook in a comment in the file so I don&amp;rsquo;t have to remember them, the command for this one: &lt;code&gt;ansible-playbook vg-install.yml --ask-vault-pass&lt;/code&gt; tells Ansible to run this playbook, and ask me for the password to decrypt the vault file.&lt;/p&gt;
&lt;p&gt;The if/then mechanism to only do something based on something earlier happening in Ansible is usually achieved with notify/handles. We put the declarative block which is optionally executed in the &lt;code&gt;handlers:&lt;/code&gt; block. The name of this block (in the case above it is &lt;code&gt;Restart vitals-glimpse&lt;/code&gt;) is specified with the &lt;code&gt;notify&lt;/code&gt; key. If either of the files are copied in, then the notify flag is set and the service is restarted.&lt;/p&gt;</description></item><item><title>Simple API endpoint in Go</title><link>https://blog.iankulin.com/simple-api-endpoint-in-go/</link><pubDate>Wed, 27 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/simple-api-endpoint-in-go/</guid><description>&lt;img src="https://blog.iankulin.com/images/gopher.png" width="219" alt=""&gt;
&lt;p&gt;I&amp;rsquo;d like a small, quick, low load endpoint on all my nodes and VM&amp;rsquo;s that exposes a text keyword indicating if that machine is okay for RAM and disk space. I&amp;rsquo;m currently using &lt;a href="https://blog.iankulin.com/tags/uptime-kuma/"&gt;Uptime Kuma&lt;/a&gt; to monitor if these machines are pingable, but I&amp;rsquo;d love a tiny bit more information from them so I&amp;rsquo;d get a &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Ntfy&lt;/a&gt; buzz on my phone if a machine is in trouble.&lt;/p&gt;
&lt;p&gt;I mentioned a couple of weeks ago that the benefit of doing it in C rather than Node.js was probably not worth the trouble, but then being a fickle developer, decided to write it in Go.&lt;/p&gt;
&lt;p&gt;This was a pretty sweet experience, it&amp;rsquo;s a nice language and the ecosystem is good. When writing such a small utility, you don&amp;rsquo;t really get a full appreciation for a language, but there is a couple of nice things going on - one I appreciated was that unused code - for example an import that&amp;rsquo;s not used, or a variable declared but not accessed is a compiler error and flagged by the intellisense as you type.&lt;/p&gt;
&lt;p&gt;In terms of the language as written, it&amp;rsquo;s fair to say C-like - there&amp;rsquo;s no weirdness like the formatting being semantic. It&amp;rsquo;s statically typed, but has good inference.&lt;/p&gt;
&lt;p&gt;The code is up on &lt;a href="https://github.com/IanKulin/vitals-glimpse"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s hard-coded to port 10321 and the route is &lt;code&gt;/vitals.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-9.37.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-9.37.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You get back this JSON. In my Uptime Kuma system, I search for the keywords &lt;code&gt;mem_okay&lt;/code&gt; and &lt;code&gt;disk_okay&lt;/code&gt; - no need to parse the JSON, it&amp;rsquo;s just an on/off status check that will show up in red on the page if there&amp;rsquo;s trouble, and ping my phone using &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;ntfy.sh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In Uptime Kuma, there&amp;rsquo;s an option when setting up a new monitor for &lt;code&gt;Http(s) Keyword&lt;/code&gt;. How this works is that it will scrape that web address and look to see if a particular keyword exists. If the keyword is present on the page, that site is marked as up, if not, it&amp;rsquo;s marked as down.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-7.47.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-15-at-7.47.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Testing the memory threshold for the screenshot above was fun:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;stress-ng --vm-bytes $(awk &amp;#39;/MemAvailable/{printf &amp;#34;%d\n&amp;#34;, $2 * 0.9;}&amp;#39; &amp;lt; /proc/meminfo)k --vm-keep -m 1
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Problems backing up LXC to NFS in Proxmox</title><link>https://blog.iankulin.com/problems-backing-up-lxc-to-nfs-in-proxmox/</link><pubDate>Sun, 24 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/problems-backing-up-lxc-to-nfs-in-proxmox/</guid><description>&lt;p&gt;If you create an unprivileged LXC container on Proxmox, then try to back it up to an NFS share, for example on a NAS, you&amp;rsquo;ll get an error when it tries to build the temporary file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.29-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.29-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The clue is in the &lt;code&gt;Permission denied&lt;/code&gt; line. It is trying to create a temporary file on my NAS, and failing because of a &lt;a href="https://blog.iankulin.com/could-it-be-a-permissions-problem/"&gt;permissions&lt;/a&gt; problem. If I try the same backup to the local storage, it works fine.&lt;/p&gt;
&lt;p&gt;The solution is to build the temporary file in the local storage. To do this, you need to edit the &lt;code&gt;/etc/vzdump.conf&lt;/code&gt; on the Proxmox node to set the &lt;code&gt;tmpdir: /tmp&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.16.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.16.14-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then if you run the backup again, it will be able to create the temporary file, and successfully copy it to the share.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.20-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-14-at-9.15.20-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t make sense to me how it has the permissions to copy the finished backup file to the share, but not create a temporary file there - but I&amp;rsquo;m not curious enough today to find out. Shout out to user &lt;a href="https://forum.proxmox.com/members/dunuin.96080/"&gt;Dunuin&lt;/a&gt; in the Proxmox &lt;a href="https://forum.proxmox.com/threads/cannot-backup-only-lxc-to-nfs-vm-works.90797/"&gt;forums&lt;/a&gt; for the suggestion to change the &lt;code&gt;tmpdir&lt;/code&gt; in &lt;code&gt;/etc/vzdump.conf&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Error wiping old drive in Proxmox</title><link>https://blog.iankulin.com/error-wiping-old-drive-in-proxmox/</link><pubDate>Thu, 31 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/error-wiping-old-drive-in-proxmox/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.19.42-pm-copy.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.19.42-pm-copy.png" width="568" alt="Error: disk/partition '/dev/sda3' has a holder (500)"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I popped in an NVME drive and freshly installed Proxmox to it, I assumed I&amp;rsquo;d just be able to wipe the SDD that had previously been the boot drive to set it up as a ZFS pool. However, when I tried to do the wipe, I was greeted with the error:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;disk/partition &amp;#39;/dev/sda3&amp;#39; has a holder (500)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I assume this means there&amp;rsquo;s a flag set on one of the Proxmox partitions to prevent accidental deletion or Proxmox thought that&amp;rsquo;s where it was running from. It&amp;rsquo;s likely that it&amp;rsquo;s related to this message I had during installation that I haven&amp;rsquo;t seen before:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_5830.jpg" alt="Detected existing &amp;lsquo;pve&amp;rsquo; Volume Group(s)! Do you want to: rename VG backed by PV &amp;lsquo;/dev/sda3&amp;rsquo; to &amp;lsquo;pve-OLD-D4DDE7DC&amp;rsquo; or cancel the installation?"&gt;&lt;/p&gt;
&lt;p&gt;Since I didn&amp;rsquo;t want to cancel the installation, I went ahead and told it okay. On the non-graphical &amp;lsquo;console&amp;rsquo; version of the installer, this message is truncated, and the only option available is abort. I guess that&amp;rsquo;s an installer bug. So if you are adding a extra boot drive to an existing Proxmox node, I suggest using the graphical installer.&lt;/p&gt;
&lt;p&gt;When I Googled around for the &amp;ldquo;has a holder&amp;rdquo; error, there were several unanswered requests for help for this, several speculative answers, and &lt;a href="https://www.reddit.com/r/Proxmox/comments/xff5ri/how_do_i_wipe_an_old_drive/"&gt;one that worked&lt;/a&gt;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/66d29d7d-bc29-4747-b92a-7fc7c790227f_text.gif" width="400" alt=""&gt;
&lt;p&gt;You need to use &lt;code&gt;fdisk&lt;/code&gt; to remove each partition. Take a note of the drive name - I could see in the Proxmox GUI that mine was sda, so the command to run was:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fdisk /dev/sda&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You probably need to have a &lt;a href="https://www.howtogeek.com/106873/how-to-use-fdisk-to-manage-partitions-on-linux/"&gt;read-up on&lt;/a&gt; &lt;code&gt;[fdisk](https://www.howtogeek.com/106873/how-to-use-fdisk-to-manage-partitions-on-linux/)&lt;/code&gt; if you&amp;rsquo;re not familiar with it, but basically, you&amp;rsquo;re in the command mode, for one of the partitions (my &lt;code&gt;sda&lt;/code&gt; had three) if you press the &lt;code&gt;d&lt;/code&gt; key here it marks that partition for deletion. Even though the error message had said it was the last partition that was causing the headache, I just went ahead and deleted all of them. There&amp;rsquo;s no warnings as you do this, and actually no changes have been made yet, that happens when you press &lt;code&gt;w&lt;/code&gt; to write the changes. No warning here either. 🙂&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.29.16-pm.png" alt="fdisk screenshot"&gt;&lt;/p&gt;
&lt;p&gt;That gave an error saying the third partition was still in use by the kernel, so I followed the advice to reboot, then I was able to wipe the drive in the Proxmox web GUI.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.30.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.30.09-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Installing a Node app on a server</title><link>https://blog.iankulin.com/installing-a-node-app-on-a-server/</link><pubDate>Tue, 22 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-a-node-app-on-a-server/</guid><description>&lt;p&gt;Before I write a fancy Ansible playbook to automatically set up the Nginx/Node combo on my web servers, it might be worth going through how to deploy a Node app so it can run on a server without you being logged in.&lt;/p&gt;
&lt;p&gt;Until now, I&amp;rsquo;ve been running my tests on my laptop, or in a server logged in as myself - sometimes detaching from tmux. But we need a bit more professional set up than that. The process will look something like this:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/hqdefault.jpg" width="150" alt=""&gt;
&lt;ul&gt;
&lt;li&gt;Install Node and npm (I&amp;rsquo;m assuming we&amp;rsquo;ve done that since I&amp;rsquo;ve covered the playbook for it before).&lt;/li&gt;
&lt;li&gt;Copy the app files over&lt;/li&gt;
&lt;li&gt;Install the dependencies&lt;/li&gt;
&lt;li&gt;Write the systemd config file&lt;/li&gt;
&lt;li&gt;Start it up&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="app-files"&gt;App files&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ll use the very simple server (&lt;code&gt;index.js&lt;/code&gt;) I&amp;rsquo;ve written for the future Ansible post. All it does is listen on port 3000 to serve a tiny piece of text if someone hits the &lt;code&gt;/api&lt;/code&gt; route.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const express = require(&amp;#39;express&amp;#39;);
const app = express();

const PORT = 3000;

app.get(&amp;#34;/api&amp;#34;, (req, res) =&amp;gt; {
 res.status(200).send(&amp;#39;Success - from /api route via node.js&amp;#39;);
});

app.listen(PORT, () =&amp;gt; {console.log(`Listening on port ${PORT}`)});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;ll also have our &lt;code&gt;package.json&lt;/code&gt;, of which the only interesting thing to notice is that we&amp;rsquo;ve got a dependency on the &lt;code&gt;express&lt;/code&gt; package which I originally installed with &lt;code&gt;npm&lt;/code&gt;. All the files for our dependencies are stored in the &lt;code&gt;./node_modules&lt;/code&gt; directory, but we don&amp;rsquo;t need to copy them to the server.&lt;/p&gt;
&lt;h3 id="where-to-put-the-app-files"&gt;Where to put the app files&lt;/h3&gt;
&lt;p&gt;If I was doing this for a commercial app, I might store the app under &lt;code&gt;/var/www/&amp;lt;app name&amp;gt;&lt;/code&gt; since that&amp;rsquo;s where a future sysadmin might look for it if they don&amp;rsquo;t have access to the playbooks. Another good place might be the home directory of the ansible/node user. Since that&amp;rsquo;s me in this case, they&amp;rsquo;re just going to go in my home directory - it makes the playbook commands shorter. We can use &lt;code&gt;scp&lt;/code&gt; to copy the files in.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-16-at-5.50.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once the files are there, we can install the dependencies with &lt;code&gt;npm install&lt;/code&gt;. This looks at the &lt;code&gt;package.json&lt;/code&gt;, then grabs them down.&lt;/p&gt;
&lt;h3 id="systemd"&gt;systemd&lt;/h3&gt;
&lt;p&gt;systemd manages the init and daemon processes in most Linux distros, so we&amp;rsquo;ll be using that to get our node app running as a service. It was &lt;a href="https://en.wikipedia.org/wiki/Systemd#History"&gt;a present from Red Hat&lt;/a&gt;. Processes that run like this need a configuration file in &lt;code&gt;/lib/systemd/system&lt;/code&gt;, ours will be called&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/lib/systemd/system/test-server.service&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Unit]
Description=index.js - test server 
After=network.target

[Service]
Type=simple
User=ian 
ExecStart=/usr/bin/node /home/ian/index.js
Restart=on-failure

[Install]
WantedBy=multi-user.target 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This file needs to say that we should wait for the network to come up before starting, what we&amp;rsquo;re running and what to do if it dies. &amp;lsquo;on-failure&amp;rsquo; means it will be restarted in pretty much any case but us stopping it cleanly. The &lt;code&gt;[multi-user.target](https://unix.stackexchange.com/questions/506347/why-do-most-systemd-examples-contain-wantedby-multi-user-target)&lt;/code&gt; bit is saying we want this service up and running for the system to be considered ready as a server.&lt;/p&gt;
&lt;p&gt;Once that file is in place, we can reload the configs, and start the service, this can be from anywhere, including a user home directory, and check it&amp;rsquo;s status.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl start test-server
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-16-at-8.10.32-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That all looks good, and if I visit the endpoint, there&amp;rsquo;s the expected response, even after we&amp;rsquo;ve logged out of the server.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-16-at-8.13.04-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Digital Ocean first impressions</title><link>https://blog.iankulin.com/digital-ocean-first-impressions/</link><pubDate>Sat, 19 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/digital-ocean-first-impressions/</guid><description>&lt;p&gt;I&amp;rsquo;ve been thinking about the time it takes me to provision a guest VM in Proxmox. I seem to remember on &lt;a href="https://www.binarylane.com.au/"&gt;BinaryLane&lt;/a&gt; it was seconds rather than minutes. This seemed to be a good excuse to use the free credit I&amp;rsquo;ve heard about for &lt;a href="https://www.linode.com/lp/free-credit-100/?promo=sitelin100-02162023&amp;amp;promo_value=100&amp;amp;promo_length=60&amp;amp;utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=11178784684_109179223363&amp;amp;utm_term=g_kwd-2629795801_e_linode&amp;amp;utm_content=466889596558&amp;amp;locationid=1000676&amp;amp;device=c_c&amp;amp;gclid=CjwKCAjw-7OlBhB8EiwAnoOEk9lQtzb_l17rAJmoU1KzhTUcWc6TF6C8KBTZU3j6tJ3d1qLWqqiRgxoC6qUQAvD_BwE"&gt;Linode&lt;/a&gt; or Digital Ocean hundreds of times in podcast adverts, so I claimed the &lt;a href="http://do.co/lnl"&gt;$200 credit for being a Late Night Linux listener&lt;/a&gt; at Digital Ocean. They extracted $5 out of me in the process, so I guess they are in front on that transaction. $200 would run a little VM for a couple of years at their rates, but of course it&amp;rsquo;s limited to two months, at the end of which I will have an account sitting there, with my credit card already recorded - so all the friction is gone if I need an internet facing machine for some purpose - which is clearly their dastardly plan&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-11-at-7.50.07-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-11-at-7.50.07-pm.png" width="351" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The process of creating a &amp;lsquo;droplet&amp;rsquo; (that&amp;rsquo;s what they call their VM&amp;rsquo;s) was straightforward - select the datacentre, machine size etc You can upload your SSH key which is a nice touch.&lt;/p&gt;
&lt;p&gt;When I got to the end of all that, I hit create and timed the boot up of the Debian 12 system I&amp;rsquo;d chosen - 42.13 seconds.&lt;/p&gt;
&lt;p&gt;I could ping the public IP, so it existed, but couldn&amp;rsquo;t ssh in as root, and didn&amp;rsquo;t know my user name. After trawling through their Getting Started docs, I found one that said to use your email that you signed up with. That didn&amp;rsquo;t make sense or work. I &lt;a href="https://www.youtube.com/watch?v=kzThZOZj1S4&amp;amp;t=417"&gt;watched a video&lt;/a&gt;, then searched further and found I should have gone into the advanced options and written a script to add a user - a sample one was provided.&lt;/p&gt;
&lt;p&gt;I destroyed the first machine and created a second one with the sample user script (which I&amp;rsquo;ve since gone back and searched for but could not find) which basically adds the user and assigns the ssh key. Once that was booted I could ssh in, but not sudo since I didn&amp;rsquo;t know the password.&lt;/p&gt;
&lt;p&gt;There is a &amp;lsquo;console&amp;rsquo; so I used that to set a password for the user the script had created, then was able to both ssh in and use sudo. I guess the idea of the script is great if you know what you&amp;rsquo;re doing and going to be creating a lot of VM&amp;rsquo;s, but this was a painful start compared to &lt;a href="https://www.binarylane.com.au/"&gt;BinaryLane&lt;/a&gt; or my homelab. I figured out afterwards, this was because I&amp;rsquo;d chosen Debian for the distro - you can&amp;rsquo;t ssh in as root. If I choose a more relaxed distro, I could do that, and create my user then patch up the root access.&lt;/p&gt;
&lt;p&gt;The rest of the experience was fine - the web interface is clear enough apart from my initial grumble. I couldn&amp;rsquo;t paste into the web console, and I&amp;rsquo;ve noticed that in Proxmox as well so I guess that&amp;rsquo;s some sort of limitation. In any case, once you&amp;rsquo;ve set up your ssh user properly you never need use it again.&lt;/p&gt;</description></item><item><title>Ansible with Secrets</title><link>https://blog.iankulin.com/ansible-with-secrets/</link><pubDate>Sun, 13 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ansible-with-secrets/</guid><description>&lt;p&gt;We wrote a nice &lt;a href="https://blog.iankulin.com/first-ansible-playbook/"&gt;little Ansible playbook&lt;/a&gt; the other day to install nginx on our web servers and ensure it was running. We were able to store the usernames in the &lt;code&gt;hosts&lt;/code&gt; inventory file using the a&lt;code&gt;nsible_ssh_user&lt;/code&gt; variable. Then, we ran the playbook with the command:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ansible-playbook web_installs.yaml --ask-become-pass&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This asked us the password to use with the usernames in the &lt;code&gt;hosts&lt;/code&gt; file. Luckily that day, it was the same username/password combo to use for sudo on every server. What happens if that&amp;rsquo;s not the case? Here&amp;rsquo;s our new hosts file for today. There&amp;rsquo;s a cool new sysadmin in town - Jane.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[vm323_deb]
100.108.154.133

[vm323_deb:vars]
ansible_ssh_user=ian

[vm324-deb]
100.77.75.14

[vm324_deb:vars]
ansible_ssh_user=jane
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We could still use &lt;code&gt;--ask-become-pass&lt;/code&gt; but it only asks us one password, and it&amp;rsquo;s highly unlikely (we hope) that Jane and Ian have chosen the same password.&lt;/p&gt;
&lt;p&gt;If you look at the inventory file above, you can see how the variables work - it&amp;rsquo;s the same variable name - Ansible swaps the correct value in for each server as it accesses them. There&amp;rsquo;s many of these variables in addition to &lt;code&gt;ansible_ssh_user&lt;/code&gt;, including &lt;code&gt;ansible_ssh_pass&lt;/code&gt;, so maybe we can do something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[vm323_deb]
100.108.154.133

[vm323_deb:vars]
ansible_ssh_user=ian
ansible_become_password=mittens96

[vm324_deb]
100.77.75.14

[vm324_deb:vars]
ansible_ssh_user=jane
ansible_become_password=GBLEzrvc8rnUFruVrCwm
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you try that, it will work. However &lt;strong&gt;it is a terrible idea to store our ssh passwords in that inventory file in plaintext.&lt;/strong&gt; They would be available to anyone who gets access to my workstation, AND when I commit my work to git, it&amp;rsquo;s getting copied somewhere, probably including github. This is such a common problem there&amp;rsquo;s &lt;a href="https://www.gitguardian.com/solutions/scan-github-for-passwords"&gt;some business&lt;/a&gt; that have come into being to scan for passwords and API keys in people&amp;rsquo;s repos.&lt;/p&gt;
&lt;p&gt;There &lt;em&gt;is&lt;/em&gt; a better way. Ansible will allow us to store the secrets in a separate file as variables, and that separate file can be encrypted while it&amp;rsquo;s on disk, and Ansible will decrypt it to use from memory then clean up after itself. There&amp;rsquo;s a couple of new (to us) things here - variables, and the encryption. Let&amp;rsquo;s look at them separately.&lt;/p&gt;
&lt;h3 id="variables"&gt;Variables&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;[Ansible variables](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html)&lt;/code&gt; is a whole subject, we&amp;rsquo;re just going to look at the minimum we need to solve out problem.&lt;/p&gt;
&lt;p&gt;We can create another file in our project, let&amp;rsquo;s call it plaintext.yaml and store our usernames and passwords in there as key: value pairs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-11.43.01-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then we need to tell our playbook to import that &lt;code&gt;plaintext.yaml&lt;/code&gt; file with &lt;code&gt;vars_files&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-11.53.25-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Then in the inventory file, we can substitute the usernames and passwords with our variables.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-11.58.55-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now, when the playbook runs, it will substitute the real values into the &lt;code&gt;host&lt;/code&gt; inventory file for us. Let&amp;rsquo;s check that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-12.03.09-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Bingo bongo. But we haven&amp;rsquo;t actually solved our security problem yet. The ssh passwords are still dangerously stored in plaintext in our file system. What we have done is learned how to have variables in an external file, pull that into the playbook and have the values substituted in. Now we need the next step - keeping those passwords safe.&lt;/p&gt;
&lt;h3 id="ansible-vault"&gt;Ansible Vault&lt;/h3&gt;
&lt;p&gt;Clearly, every serious use of Ansible is going to have this issue (of needing ssh credentials, but not wanting to give them away to hackers) so of course, there is an elegant solution for it.&lt;/p&gt;
&lt;p&gt;What if the file with all the passwords was stored encrypted, then only decrypted for use by our playbook, and it was never saved anywhere as plaintext? That solves the problem.&lt;/p&gt;
&lt;p&gt;Ansible has a tool for this called &lt;a href="https://docs.ansible.com/ansible/2.8/user_guide/vault.html"&gt;Ansible Vault&lt;/a&gt;. We&amp;rsquo;ll create the yaml file with our variables with that tool, and it will be saved encrypted. When we run the playbook we&amp;rsquo;ll get it to ask us for the password to decrypt the file. It will do that in memory and run the playbook.&lt;/p&gt;
&lt;p&gt;The command to create our file, which we&amp;rsquo;ll call &lt;code&gt;vault.yaml&lt;/code&gt; will be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible-vault create vault.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will ask us to enter the password we want to use. The strength of this password needs to be good. If it&amp;rsquo;s crackable, you are giving away root access to your systems. And it&amp;rsquo;s in a file, not in a login situation where you can time out after three logins. Whoever has the file has the time to brute force password of the the &lt;a href="https://www.ipswitch.com/blog/use-aes-256-encryption-secure-data"&gt;AES 256&lt;/a&gt; encryption.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/h4c7m5z2a2b71-copy.jpg" width="460" alt=""&gt;
&lt;p&gt;Once you&amp;rsquo;ve entered your strong password twice, it will open up the new file in the default editor - probably vim. You may need help to use this editor.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-12.46.35-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re done, save the file and exit. What does this file look like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-12.51.38-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Yep. That should do it. We still need to tell our playbook to use this file instead of the other one.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;--- 
- name: nginx for all web servers
 vars_files: ./vault.yaml
 hosts: all
 become: yes
 tasks: 
 - name: nginx installed
 apt:
 name: nginx
 state: latest
 - name: nginx running
 service: 
 name: nginx
 state: started
 enabled: yes
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And when we run the playbook, we need to let it know to ask us the vault password.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible-playbook web_installs.yaml --ask-vault-pass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-06-at-1.00.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you need to edit the secrets file in the future, the command for that would be&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ansible-vault edit vault.yaml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once again it will ask the password, and once again it will open up in vim.&lt;/p&gt;</description></item><item><title>Finding the host IP from inside a Docker container</title><link>https://blog.iankulin.com/finding-the-host-ip-from-inside-a-docker-container/</link><pubDate>Mon, 07 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/finding-the-host-ip-from-inside-a-docker-container/</guid><description>&lt;p&gt;Having successfully set up and tested my node.js api handling app behind nginx on a development VM in the homelab, I decided to move it to my VPS so I could start using it for real. I had a bit of trouble finding the nginx.conf files on the VPS, until I remembered I was running nginx in a docker container on this machine!&lt;/p&gt;
&lt;p&gt;I got everything set up, I could hit the domain in a web browser and get served the static page, and I could &amp;lt;domain_name&amp;gt;:3000/api/gnp_temp.txt and get the file delivered by the node script, but if I tried &amp;lt;domain_name&amp;gt;/api/gnp_temp.txt - &amp;ldquo;Bad Gateway&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I looked at the nginx.conf over and over - it seemed fine. The location block was clearly being triggered, otherwise I&amp;rsquo;d be getting a 404. I dived into stack overflow to no avail. It was if http://localhost:3000 just wasn&amp;rsquo;t working. But it definitely was when I &lt;code&gt;curl&lt;/code&gt;ed it from the command line.&lt;/p&gt;
&lt;p&gt;In desperation I started writing out an explanation to ChatGPT about the setup and my problem, and before I pressed enter realised - from nginx&amp;rsquo;s point of view, http://localhost:3000 was an address &lt;em&gt;inside&lt;/em&gt; the container 🤦, what I needed was the address of the host, from the point of view of the docker container. Surely that must be a common requirement that&amp;rsquo;s been solved.&lt;/p&gt;
&lt;p&gt;From reading around, it seems like I should be able to just substitute &lt;code&gt;host.docker.internal&lt;/code&gt; but that didn&amp;rsquo;t seem to work. I opened a shell into the container to look at &lt;code&gt;ip a&lt;/code&gt; or &lt;code&gt;/sbin/ip route|awk '/default/ { print $3 }'&lt;/code&gt; but of course these containers are slim installs without the general tools you need.&lt;/p&gt;
&lt;p&gt;Docker networking is a whole thing that I should learn, but I haven&amp;rsquo;t yet, I just start up containers with whatever the defaults for networking are. But I figured the host must be part of the docker network, and a quick look in &lt;code&gt;ip a | grep docker&lt;/code&gt; produced this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;3: docker0: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc noqueue state DOWN group default 
 inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Bingo. I popped that IP address into my nginx.conf and everything started working perfectly. That was forty minutes of learning I wouldn&amp;rsquo;t have had to live through if I could have just turned around to a work colleague and asked.&lt;/p&gt;</description></item><item><title>nginx in Front of a node.js app</title><link>https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/</link><pubDate>Fri, 04 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/</guid><description>&lt;p&gt;NGINX is a great webserver and reverse proxy - as in it can hand off requests to other web-servers. That&amp;rsquo;s the situation I want to have set up on my VPS. I want NGINX to handle incoming requests - some of them will just be sorted out by returning static HTML, others (like the weather api I&amp;rsquo;ve been playing with) need to be handed off to other services to respond to.&lt;/p&gt;
&lt;p&gt;In the situation I&amp;rsquo;m looking at, I want requests that have the route /api (eg example.com/api/weather) to be passed to a node.js program I&amp;rsquo;ve written. All the other http requests should just be treated as requests for static pages and dealt with by NGINX.&lt;/p&gt;
&lt;p&gt;So I guess is part V of my adventures in the weather API, if you just want to know how to set up NGINX to serve static pages AND pass some routes off to node, you don&amp;rsquo;t need to be up to date on these.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/"&gt;Outside Temperature From an API in a Shell Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/complicating-the-temperature-api/"&gt;Complicating the Temperature API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;Using Node.js to serve a static file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;How to Deploy a Node.js App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nginx-configuration"&gt;nginx Configuration&lt;/h3&gt;
&lt;p&gt;Once nginx and node.js are installed (with &lt;a href="https://gist.github.com/IanKulin/bd6d1a78f9a9fa9a859384a26ca95235"&gt;the Ansible script&lt;/a&gt; if you want to rock the dev ops tattoo) you&amp;rsquo;ll need to configure nginx. On my Debian systems, the config file &lt;code&gt;nginx.conf&lt;/code&gt; is in &lt;code&gt;/etc/nginx/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a line in that file that includes all the &lt;code&gt;*.conf&lt;/code&gt; files in &lt;code&gt;/etc/nginx/conf.d&lt;/code&gt;. This is a common pattern I see in some distros - the main config files are not really meant to be messed with, but then there&amp;rsquo;s a directory to add config files to whihc are included. The theoretical advantage of this is that the distro maintainers can roll out a new version of a package and change the main config file, and your stuff will still work.&lt;/p&gt;
&lt;p&gt;The way they have done this with the nginx.conf means that the only changes we can make in the &lt;code&gt;conf.d&lt;/code&gt; directory are to do with virtual hosts, but that&amp;rsquo;s going to be 99% of the things we would want to change. So much so, I&amp;rsquo;m not even going to show you the &lt;code&gt;nginx.conf&lt;/code&gt; file, just our little &lt;code&gt;/etc/nginx/conf.d/nodeapi.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; server {
 listen 80;
 server_name 192.168.100.40;

 # Serve static files
 root /var/www;

 # pass api requests to node
 location /api {
 proxy_pass http://localhost:3000;
 proxy_set_header Host $host;
 }
 }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Okay, we are listening on port 80, and this &amp;ldquo;server block&amp;rdquo; is only for requests like http://192.168.100.40. The purpose of &lt;code&gt;server_name&lt;/code&gt; is that we might run the websites for several domains from one nginx installation. For example, we might be serving example.com and otherexample.com from the same VPS.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;root /var/www;&lt;/code&gt; - tells nginx to grab the files from that directory, so that&amp;rsquo;s the static web server part.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;location /api {&lt;/code&gt; - is telling nginx &amp;ldquo;all the requests with /api on the end are dealt with differently, look in this block for instructions&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_pass http://localhost:3000;&lt;/code&gt; - send them all to a server on this machine listening on port 3000. This is where the node/express server is running.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_set_header Host $host;&lt;/code&gt; - it doesn&amp;rsquo;t matter for the purposes of this api, but it&amp;rsquo;s often nice to tell the server being proxied who the real host receiving the request is. If we didn&amp;rsquo;t do this, the node app would only be able to see that &amp;ldquo;localhost&amp;rdquo; was making a request. By doing this, it knows the request was to the server running nginx, in this case 192.168.100.40, but usually a real domain name. This might be needed if our node app was also servicing more than one domain.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. We&amp;rsquo;re done. You do need to have your node app running on port 3000 on the same machine, but as long as that&amp;rsquo;s happening this should all be working. Do remember to restart nginx each time you make a config changes with &lt;code&gt;sudo service nginx restart&lt;/code&gt;, and it would also be good practice to check the config files with &lt;code&gt;sudo nginx&lt;/code&gt; -t before that.&lt;/p&gt;</description></item><item><title>First Ansible Playbook</title><link>https://blog.iankulin.com/first-ansible-playbook/</link><pubDate>Wed, 26 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/first-ansible-playbook/</guid><description>&lt;p&gt;In the &lt;a href="https://blog.iankulin.com/getting-started-with-ansible/"&gt;previous post&lt;/a&gt;, we looked at getting up and running with Ansible, including using the ad-hoc mode to send commands to our servers. We had a inventory file called hosts that had groups of server IP addresses and a simple &lt;code&gt;ansible.cfg&lt;/code&gt; file that pointed to our inventory file.&lt;/p&gt;
&lt;h3 id="playbooks"&gt;Playbooks&lt;/h3&gt;
&lt;p&gt;Ansible playbooks are used to collect together a description of the state we want in a server. When the playbook is executed, Ansible figures out what things need need changed, and changes them. If you&amp;rsquo;re used to the procedural nature of a bash script, where things proceed from one step to the next, and there might be decision branches, this requires an adjustment in your thinking. This is similar to the adjustment I had getting my head around &lt;a href="https://betterprogramming.pub/swiftui-understanding-declarative-programming-aaf05b2383bd"&gt;SwiftUI&lt;/a&gt;, and moving from JS to &lt;a href="https://levelup.gitconnected.com/why-react-is-declarative-a300d1e930b7?gi=3d11485226b4"&gt;React&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before we dive in and look at a playbook, I should probably say a couple of things about the &lt;a href="https://www.tutorialspoint.com/yaml/yaml_basics.htm"&gt;YAML&lt;/a&gt; format used for these files. It&amp;rsquo;s yet another attempt to strike a compromise between human readable and machine processable files. Spacing is important, it doesn&amp;rsquo;t like tabs, it&amp;rsquo;s case sensitive, and begins with three hyphens. The rest, you&amp;rsquo;ll figure out.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the files we currently have in our working directory:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.16-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.16-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.11.02-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The config file just specifies our inventory file, and the inventory file (named &lt;code&gt;hosts&lt;/code&gt;) lists the servers in groups, and provides some &lt;em&gt;variables&lt;/em&gt; for the servers.&lt;/p&gt;
&lt;p&gt;Our web servers are going to need something to serve web pages. Let&amp;rsquo;s write a playbook to ensure they have NGINX installed. If you don&amp;rsquo;t know what &lt;a href="https://www.nginx.com/"&gt;NGINX&lt;/a&gt; is, don&amp;rsquo;t worry about it, it&amp;rsquo;s a web server.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.38.27-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.38.27-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- name:&lt;/code&gt; - YAML files are hierarchical. Ansible YAML files are a collection of &lt;em&gt;plays&lt;/em&gt;. This file only has one play, named &amp;ldquo;nginx for all web servers&amp;rdquo;. All the plays will be at the top level like this starting with a single hyphen in column 1. Names are great; pick good ones and you won&amp;rsquo;t need much in the way of comments. These names also appear in the output, helping anyone using the playbook to understand what&amp;rsquo;s happening.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hosts: web&lt;/code&gt; - tells Ansible that we are only running this play against our &lt;code&gt;web&lt;/code&gt; servers. These are defined in the &lt;code&gt;hosts&lt;/code&gt; file that we kept from the last post. If you look back up at the top for that file, you can see we&amp;rsquo;re specifying it for 192.168.100.37 and 192.168.100.38&lt;/p&gt;
&lt;p&gt;&lt;code&gt;become: yes&lt;/code&gt; - To install packages with &lt;code&gt;apt&lt;/code&gt;, we need to &lt;code&gt;sudo&lt;/code&gt;. &lt;code&gt;become yes&lt;/code&gt; is telling Ansible that we need to do this. I guess if we were already the &lt;code&gt;root&lt;/code&gt; user we wouldn&amp;rsquo;t have to do that, but in our &lt;code&gt;hosts&lt;/code&gt; file, we&amp;rsquo;ve said to use the user &lt;code&gt;ian&lt;/code&gt; so &lt;code&gt;ssh&lt;/code&gt; in. We&amp;rsquo;ll see later how to deal with needing the password for this escalation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tasks:&lt;/code&gt; - In our hierarchy, each &lt;em&gt;play&lt;/em&gt; consists of a number of &lt;em&gt;tasks&lt;/em&gt;. Here&amp;rsquo;s our list of tasks. Because we&amp;rsquo;re changing levels, they&amp;rsquo;ll be indented.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- name:&lt;/code&gt; - This time, it&amp;rsquo;s the name of the task. Again, pick good ones.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt:&lt;/code&gt; Ansible functionality is organised according to modules. Here we are saying we are going to use the &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/apt_module.html"&gt;apt module&lt;/a&gt;. &lt;code&gt;apt&lt;/code&gt; is the command to install packages on Debian flavoured systems&lt;/p&gt;
&lt;p&gt;&lt;code&gt;name : nginx&lt;/code&gt; - This time, it&amp;rsquo;s the name of the package we want installed. It&amp;rsquo;s the same name as you would have used if using &lt;code&gt;apt&lt;/code&gt; manually.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;state: latest&lt;/code&gt; - we&amp;rsquo;re saying we want the nginx package to be installed, and we want it to be the latest one. This is were you should really be noticing the declarative nature of the playbook. We could also say &lt;code&gt;state: absent&lt;/code&gt; and Ansible would uninstall it if it was installed, or &lt;code&gt;state: present&lt;/code&gt; in which case Ansible would just check it&amp;rsquo;s there but not worry about the version.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;- name:&lt;/code&gt; - Okay, we&amp;rsquo;re back up at the lists of tasks level. Here&amp;rsquo;s the name of a new task.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;service:&lt;/code&gt; In the previous task we were using the apt module. This task is going to use the &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/service_module.html"&gt;service module&lt;/a&gt;. If you&amp;rsquo;re wondering how you get to know what things are in each module, the &lt;a href="https://docs.ansible.com/ansible/latest/module_plugin_guide/index.html#"&gt;documentation at Ansible&lt;/a&gt; is pretty great. Sometimes you&amp;rsquo;ll get pretty close by thinking of what you&amp;rsquo;re doing. In this case, we want to check if the NGINX service is running, and if not start it - so it&amp;rsquo;s logical that the module we want is going to be &lt;code&gt;service&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;name : nginx&lt;/code&gt; - This time, it&amp;rsquo;s the name of the service.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;state: started&lt;/code&gt; - declaratively saying what we want the state of the NGINX service to be.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;enabled: yes&lt;/code&gt; - it will also be started on a reboot.&lt;/p&gt;
&lt;p&gt;Phew. Okay. We want NGINX to be installed on these machines, and for the service to be running and for that to still be the case after a reboot. Let&amp;rsquo;s run this playbook and see what happens. Here&amp;rsquo;s the command we&amp;rsquo;re going to do that with.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible-playbook web_installs.yaml --ask-become-pass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;--ask-become-pass&lt;/code&gt; piece of this command is telling Ansible to ask us for the password for this user so it can have sudo privileges to install things. We could have just added the password in the hosts file like we have the user name, but that would be quite insecure. Especially when we push our code up to github. Scanning pubic github commits for passwords and API keys is a popular pastime.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.38.19-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;After asking me for the password, Ansible has correctly identified the two servers and gathered facts from them. The facts are a lot of information that&amp;rsquo;s then stored in variables that we can then use in our playbooks. For example this playbook is assuming a Linux distro that uses the apt package manager. If we wanted to check for that, one of the facts variables would contain the distro name and we could use that to conditionally use apt or some other package manager.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ve probably noticed the colours. Green messages mean something&amp;rsquo;s in the correct state, yellow means it wasn&amp;rsquo;t in the correct state before, but is now, and red means it&amp;rsquo;s not in the correct state, and couldn&amp;rsquo;t be made to be for some reason.&lt;/p&gt;
&lt;p&gt;Since this is the first time this playbook&amp;rsquo;s been run against these servers, we expected the &amp;rsquo;nginx installed&amp;rsquo; tasks to be yellow for both servers. The highlighted IP address under &amp;rsquo;nginx running&amp;rsquo; is just because I was copying it to go and check the web server was working. Let&amp;rsquo;s have a look.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-3.36.22-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well done Ansible.&lt;/p&gt;
&lt;p&gt;In regard to those yellow messages where Ansible found that NGINX wasn&amp;rsquo;t installed, so it went ahead and installed them, you might be thinking &amp;ldquo;if we run the playbook again, shouldn&amp;rsquo;t they be green?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-2.50.42-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s our first playbook done. We&amp;rsquo;ve only learned the commands for installing packages and working with services, but Ansible can do pretty much anything. Certainly anything you can do by sshing in and running a script of some kind. I don&amp;rsquo;t think I want to go any further with trying to show the range of things that can be accomplished (although it is tempting to now install a web page into our servers) - it makes more sense for you to just find what you need as you need it.&lt;/p&gt;
&lt;p&gt;There is however one problem I ran into almost immediately and couldn&amp;rsquo;t find a simple description of that I&amp;rsquo;ll cover in the next post. Every Saturday morning, I ssh into my local and remote servers (15 of them) and run &lt;code&gt;apt update&lt;/code&gt; and &lt;code&gt;apt upgrade&lt;/code&gt;. You can see from the yaml above, that&amp;rsquo;s going to be quite easy to automate with Ansible and save me heaps of time and effort. My problem is - all my servers have unique user names and passwords. It&amp;rsquo;s not possible to just add a &amp;ndash;ask-become-pass to my command; that would only work for the first one.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll look at how to solve that securely in a future post.&lt;/p&gt;</description></item><item><title>Proxmox 8.0 Install</title><link>https://blog.iankulin.com/proxmox-8-0-install/</link><pubDate>Sun, 23 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-8-0-install/</guid><description>&lt;p&gt;I&amp;rsquo;m normally a x.1 release type of sysadmin, but the increasing temptation of installing Proxmox 8.0 while I&amp;rsquo;ve got some time off, and the fact that I&amp;rsquo;ve got a cluster, so I can just move the VM&amp;rsquo;s around all adds up to thinking I&amp;rsquo;ll do that today.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/cluster-2.png" width="328" alt=""&gt;
&lt;p&gt;Here&amp;rsquo;s how my system works. It consists of three HP-800 mini G2&amp;rsquo;s. &lt;code&gt;pve-prod1&lt;/code&gt; is a bit fancier - i7 6700T and 32GB, the other two are i5 6500T and 16GB. The production VM&amp;rsquo;s use the local SSD but backups go to the NAS. All the machines are currently running Proxmox 7.4. They are not clustered in the proper sense - I don&amp;rsquo;t need high availability, and I don&amp;rsquo;t want to run them all the time. &lt;code&gt;pve-prod1&lt;/code&gt; runs 24/7 and I just power up &lt;code&gt;pve-dev1&lt;/code&gt; when I&amp;rsquo;m working on something.&lt;/p&gt;
&lt;p&gt;The intention is that although I&amp;rsquo;m not on high availability, I can quickly come back from a machine failure by powering &lt;code&gt;pve-prod2&lt;/code&gt; up and restoring from the latest VM backup from the NAS. &lt;code&gt;pve-prod1&lt;/code&gt; does not have a full load yet (I&amp;rsquo;m slowly cancelling cloud services and moving them in-house) but once it does, I&amp;rsquo;d have the capacity to fully replace it by sharing any guests between &lt;code&gt;pve-prod2&lt;/code&gt; and &lt;code&gt;pve-dev1&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="migration-plan"&gt;Migration plan&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/migration-1.png" width="273" alt=""&gt;
&lt;p&gt;Currently &lt;code&gt;pve-prod1&lt;/code&gt; is only running two guests, jellyfin, and a docker host with a collection of smallish services. The plan is to move those to &lt;code&gt;pve-prod2&lt;/code&gt;, check everything is working, then install the new Proxmox 8 onto &lt;code&gt;pve-prod1&lt;/code&gt;. Apart from giving me the opportunity to do that, it&amp;rsquo;s a good test of the plan for recovering from a &lt;code&gt;pve-prod1&lt;/code&gt; failure. I&amp;rsquo;ll live off it for a few days to ensure that it&amp;rsquo;s a viable process.&lt;/p&gt;
&lt;p&gt;A small hitch with this is that the RAM in &lt;code&gt;pve-prod1&lt;/code&gt; cost me $100, and I didn&amp;rsquo;t want to not use it, so I created the jellyfin VM with 16GB RAM. It&amp;rsquo;s a simple matter to stop it, give it less, and restart it - except it seems to be using it all.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-7.31.59-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see from this, I tried shutting it down and restarting - thinking that the memory use might climb up slowly as the app was used, but it just went straight back to 15GB. In a way, I approve of a VM using the memory I&amp;rsquo;ve given it - presumably it is caching or something. Jellyfin should certainly be able to run on a machine with much less memory, so I suppose I&amp;rsquo;ll stop it, back it up, and try it in a smaller VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-7.42.58-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Yep, that works fine. And I can&amp;rsquo;t notice any difference in the app performance. So I stopped it, backed it up, and restored onto prod2. And immediately bumped into a couple of problems when I tried to start it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.52.34-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There was two hardware incompatibilities - the first was that on prod1 I had passed through the GPU from the host (in an unsuccessful attempt to use quicksync hardware transcoding for video). I don&amp;rsquo;t need that, so that gets deleted out of the &amp;lsquo;hardware&amp;rsquo; for the VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.47.00-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And the second was that I still had the Debian 11 ISO mounted in the &amp;lsquo;cd-rom&amp;rsquo;. Lol - the Debian installer specifically tells you to remove this before it reboots. That can be removed exactly as I had done for the GPU pass through, and the VM boots fine, and the app tests out ok.&lt;/p&gt;
&lt;p&gt;The first time I ever did this - move a guest VM from one lot of hardware to another, then boot it up and all my apps are working perfectly on their old IP addresses - I was amazed and danced around in excitement. I didn&amp;rsquo;t dance today, but it is so cool.&lt;/p&gt;
&lt;p&gt;Interestingly, it&amp;rsquo;s decided to use much less RAM now. I caused that increase at the end of the graph by rescanning the media library, then browsing through all the titles so the cover images would have to be loaded - so perhaps it&amp;rsquo;s the web server caching them all. It&amp;rsquo;s hard to know for sure without some objective measurements, but I suspect the app was crisper and more responsive than before. In any case, it certainly wasn&amp;rsquo;t any worse.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-9.02.56-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Moving the docker host over was straightforward and only took five minutes of downtime as it&amp;rsquo;s a smaller image. I guess a lot of that time is just my 1GB network limitation or the spinning disk transfer speed from the NAS - the docker hoats was 4GB and Jellyfin 14GB.&lt;/p&gt;
&lt;h3 id="nuke-and-pave"&gt;Nuke and pave&lt;/h3&gt;
&lt;p&gt;I try and keep my hosts very clean, so wiping them and starting over is no biggie, but since this node has been up I have installed a chron job for &lt;a href="https://blog.iankulin.com/linux-shell-script-for-temperature-logging/"&gt;temperature logging&lt;/a&gt;. I&amp;rsquo;ve documented that in a blog post so I&amp;rsquo;ll be able to recreate it, but this sort of thing is the reason I&amp;rsquo;m interested in &lt;a href="https://blog.iankulin.com/getting-started-with-ansible/"&gt;Ansible&lt;/a&gt;. Another project while I&amp;rsquo;ve got some time will be to recreate that on the new machine with Ansible so it&amp;rsquo;s trivial to restore in future. I pulled the temperature log file down though - because who doesn&amp;rsquo;t like eighty thousand data points.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/temp1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;There is a &lt;a href="https://pve.proxmox.com/wiki/Upgrade_from_7_to_8"&gt;published process to upgrade Proxmox&lt;/a&gt; from 7.x to 8, so I briefly considered it, but fresh installs are generally less likely to lead to drama, especially this early in the major release cycle. Plus, I keep my installs clean to allow it - this is a freedom allowed by my sysadmin discipline along with the investment in redundant hardware so there&amp;rsquo;s zero time pressure while I&amp;rsquo;m doing it.&lt;/p&gt;
&lt;h3 id="run-book-for-new-proxmox-install"&gt;Run Book for New Proxmox Install&lt;/h3&gt;
&lt;p&gt;My install process for Proxmox goes something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flash the ISO onto a USB drive with &lt;a href="https://etcher.balena.io/"&gt;Balena Etcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Plug in the USB drive, my bluetooth keyboard/mouse USB, and the screen - I&amp;rsquo;ve got a special long HDMI cord that reaches from my desk to the servers&lt;/li&gt;
&lt;li&gt;Boot up, mashing the boot menu key (F9 on my G2&amp;rsquo;s)&lt;/li&gt;
&lt;li&gt;Follow my nose through the prompts - since this is an existing server, the DHCP serves up the correct IP address&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh&lt;/code&gt; into it to check everything&amp;rsquo;s fine. Since this IP was already in my known hosts file, I had to go an delete it out&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh-copy-id&lt;/code&gt; to get my ssh keys across&lt;/li&gt;
&lt;li&gt;Update the repositories - by default, Proxmox comes set up to use with a subscription. I wish they had a lower tier and I&amp;rsquo;d by one since it gives me so much joy - even if it didn&amp;rsquo;t remove the nags. In the meantime, you can follow the instructions &lt;a href="https://pve.proxmox.com/wiki/Package_Repositories#sysadmin_no_subscription_repo"&gt;here&lt;/a&gt; to set it up to use the non-subscription repoistories:
&lt;ul&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; to add &lt;code&gt;deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list.d/pve-enterprise.list&lt;/code&gt; to comment out the line in there&lt;/li&gt;
&lt;li&gt;and a new one that&amp;rsquo;s not mentioned on that wiki page, edit &lt;code&gt;/etc/apt/sources.list.d/ceph.list&lt;/code&gt; to comment out the line in there. I don&amp;rsquo;t know where that leaves you if you are using Ceph (which is a cool file system if you&amp;rsquo;re using high availability) but I&amp;rsquo;m not, so all good. If you don&amp;rsquo;t do this, you&amp;rsquo;ll get errors like &lt;code&gt;E: Failed to fetch https://enterprise.proxmox.com/debian/ceph-quincy/dists/bookw orm/InRelease 401 Unauthorized IP: 103.76.41.50 4431 E: The repository &amp;quot;https://enterprise.proxmox.com/debian/ceph-quincy bookworm In Release' is not signed.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run the updates with &lt;code&gt;apt update&lt;/code&gt; &amp;amp;&amp;amp; &lt;code&gt;apt upgrade&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install the certificate - you need SSL setup for the web interface if you want Chrome to let it save your password, which I do. Also the red &lt;em&gt;insecure&lt;/em&gt; message bugs me
&lt;ul&gt;
&lt;li&gt;Log into the web interface at https://&lt;ip address&gt;:8006 - you&amp;rsquo;ll need to jump through all those hoops to take on the responsibility of opening an unsecured site&lt;/li&gt;
&lt;li&gt;If you click on the node, then certificates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-12.08.29-pm.png" alt=""&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;You can open up that certificate, and copy out the raw certificate, paste it into a text editor and save it somewhere. I drag that into my macOS keychain app. It shows up with a red cross, but if you open it up you can mark it as &amp;ldquo;always trust&amp;rdquo;&lt;/li&gt;
&lt;li&gt;We&amp;rsquo;re not done yet, now back in Chrome, click on the &lt;em&gt;insecure&lt;/em&gt; message next to the URL. Go into &lt;em&gt;Site Settings&lt;/em&gt; | &lt;em&gt;Insecure Content&lt;/em&gt; and change it to &lt;em&gt;Allow&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Almost there - at the top of those settings is a button to clear the cache, do that&lt;/li&gt;
&lt;li&gt;Reload the page. Profit.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then I &lt;a href="https://tailscale.com/kb/1031/install-linux/"&gt;install Tailscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Last of all, add my NAS to the storage. I use NFS. The only trick here is to go into the dropdown of what type of content is on that storage, and select everything&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-12.17.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. Nice new Proxmox. I&amp;rsquo;ll leave my production VM&amp;rsquo;s on pve-prod2 for a week, and move all of my dev work over to this machine so it gets some exercise before I upgrade the other machines.&lt;/p&gt;
&lt;h3 id="tailscale"&gt;Tailscale&lt;/h3&gt;
&lt;p&gt;The only small issue I ran into (apart from the Ceph repository) was I couldn&amp;rsquo;t access the machine via it&amp;rsquo;s &amp;ldquo;magic DNS&amp;rdquo; Tailscale name. Since it was going to be the same name as a machine in my existing network, I&amp;rsquo;d thought ahead and deleted the old one out via the &lt;a href="https://login.tailscale.com/admin/machines"&gt;Tailscale machines&lt;/a&gt; page, but even so, it wouldn&amp;rsquo;t connect from my laptop.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-11.45.38-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I assume the old Tailscale IP address was cached somewhere, and fixed it by turning Tailscale off and on again on my laptop.&lt;/p&gt;</description></item><item><title>Getting Started with Ansible</title><link>https://blog.iankulin.com/getting-started-with-ansible/</link><pubDate>Wed, 19 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-started-with-ansible/</guid><description>&lt;p&gt;Ansible is a system for executing commands on remote systems. It allows a declarative approach - so if you run a playbook (the system configuration files are called playbooks) that says a system has a Docker container running Jellyfin, Ansible will check if that&amp;rsquo;s true, and if not, make it so. Ansible is best used when you have a large number of systems to maintain, but even with a small number, it serves to document systems as well as to automate their creation.&lt;/p&gt;
&lt;p&gt;Since, with Ansible, system configurations can be completely described, it&amp;rsquo;s a step in the journey to &amp;ldquo;infrastructure as code&amp;rdquo; and allows infrastructure to be version controlled, and lends itself to Git-Ops where you push a change to a playbook file, and it&amp;rsquo;s executed to make that description of the configuration reality on your servers. The list of servers is stored in a file called the inventory.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=wgQ3rHFTM4E"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-02-at-11.28.10-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve considered implementing it a couple of times, but put it off as soon as I started looking at these complicated yaml files. Jeff Geerling&amp;rsquo;s &lt;a href="https://www.ansiblefordevops.com/"&gt;&amp;ldquo;Ansible for DevOps&amp;rdquo;&lt;/a&gt; seemed like the perfect place to start, but then he uses Vagrant and VirtualBox in his early examples, and Vagrant&amp;rsquo;s integration with Ansible means things are not being done in a standard way and I couldn&amp;rsquo;t follow along without mirroring his setup. I don&amp;rsquo;t want to run VM&amp;rsquo;s on my laptop, I want to use my homelab VMs or a VPS - both of which I think would be a more common setup.&lt;/p&gt;
&lt;p&gt;This mini guide is just a start. I&amp;rsquo;ll step through to the point where you have a yaml file describing a system configuration that can be applied to a VM to install some software. After that, you probably want to buy Jeff&amp;rsquo;s book, hit up some &lt;a href="https://www.youtube.com/watch?v=wgQ3rHFTM4E"&gt;good v&lt;/a&gt;ideos, or head to the &lt;a href="https://docs.ansible.com/"&gt;Ansible documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-02-at-11.16.33-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-02-at-11.16.33-am.png" width="118" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For this to be helpful to you, you probably need to have been mucking about running Linux servers. You know how to ssh into them and have set up key pairs to allow that without typing your password each time. You can write a bash script (but don&amp;rsquo;t want to), You know how to install software with apt/yum/pip/homebrew etc. You should go and install it now. Note that you also need python (preferably 3) on the host.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve saved a run book of the things you need to do to recreate particular setups or deal with common issues, then you are at the exact point that Ansible is going to make your life better.&lt;/p&gt;
&lt;p&gt;Get &lt;a href="https://docs.ansible.com/ansible/latest/installation_guide/installation_distros.html"&gt;Ansible installed&lt;/a&gt;, you do need an up to date Python. You also need to have ssh set up for each of the nodes (servers) you are going to manage, preferably including using keys rather than passwords.&lt;/p&gt;
&lt;h3 id="starting-concepts"&gt;Starting Concepts&lt;/h3&gt;
&lt;p&gt;Ansible can execute &lt;em&gt;playbooks&lt;/em&gt; which are yaml files setting out the actions needed or final state of the node to be achieved. Alternatively, single commands can be executed from the command line in &amp;lsquo;ad-hoc mode&amp;rsquo;. When setting things up, ad-hoc mode is a good starting place to check you&amp;rsquo;ve installed everything correctly since it&amp;rsquo;s simpler.&lt;/p&gt;
&lt;p&gt;Ansible &lt;em&gt;modules&lt;/em&gt; are bits of code to support particular pieces of functionality. You could think of them as code libraries. For example, there&amp;rsquo;s an &lt;code&gt;apt&lt;/code&gt; module that enables Ansible to execute commands related to package management on the Debian family of Linux distros. Similar to code libraries, you&amp;rsquo;ll need to know which library is needed for the functionality you want to use. Luckily, Ansible&amp;rsquo;s documentation is excellent, and as with your programming, you&amp;rsquo;ll soon become familiar with the ones you use all the time.&lt;/p&gt;
&lt;h3 id="demo-environment"&gt;Demo Environment&lt;/h3&gt;
&lt;p&gt;For the following examples, I&amp;rsquo;ve set up three virtual machines (VM&amp;rsquo;s) 192.168.100.37 - 192.168.100.39 running Debian. I use Proxmox on my servers, so it looks a bit like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-6.46.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re trying things from a single machine, you could install something like &lt;a href="https://www.virtualbox.org/"&gt;VirtualBox&lt;/a&gt; to create VM&amp;rsquo;s, or I&amp;rsquo;d probably recommend just commissioning a VPS on &lt;a href="https://www.linode.com/lp/podcasts/?ifso=ssh&amp;amp;utm_source=podcast&amp;amp;utm_medium=audio&amp;amp;utm_campaign=ssh"&gt;Linode&lt;/a&gt; or &lt;a href="https://cloud.digitalocean.com/registrations/new"&gt;Digital Ocean&lt;/a&gt;. They both have deals whereby you get a dollar amount credit for signing up, for the minimal machine you need to try these things out, you&amp;rsquo;re probably looking at a cost of $0.30 an hour. I&amp;rsquo;m in Australia, so my VPS&amp;rsquo;s are on &lt;a href="https://www.binarylane.com.au/register"&gt;Binary Lane&lt;/a&gt; which costs less that AUD5 a month for a low-end instance.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running against multiple machines, you&amp;rsquo;ll make your life easier by having the same user name on each one. For example, the commands I use to &lt;code&gt;ssh&lt;/code&gt; into mine are:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh ian@192.168.100.37
ssh ian@192.168.100.38
ssh ian@192.168.100.39
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="inventory"&gt;Inventory&lt;/h3&gt;
&lt;p&gt;Ansible has the concept of an &lt;em&gt;Inventory&lt;/em&gt;. The Inventory is a text file of the servers/nodes (I&amp;rsquo;m just going to say nodes from now on). We need this inventory whether using playbooks or ad-hoc commands. Here&amp;rsquo;s mine, which I&amp;rsquo;ve saved in the directory I&amp;rsquo;m working from as &lt;code&gt;hosts&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;192.168.100.37
192.168.100.38
192.168.100.39
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that these could also be domain names if your nodes are set up on DNS.&lt;/p&gt;
&lt;h3 id="first-command"&gt;First Command&lt;/h3&gt;
&lt;p&gt;Finally, we&amp;rsquo;re at the point we can run something. Let&amp;rsquo;s try this command to find the host name of each node. There&amp;rsquo;s a lot going on, so we&amp;rsquo;ll break it down after we&amp;rsquo;ve looked at the output.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible -i hosts all -u ian -a &amp;#34;hostname&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-7.41.00-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s break down all those arguments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-i hosts&lt;/code&gt; - the inventory flag points to the inventory file. In my example the file is named &amp;ldquo;hosts&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;all&lt;/code&gt; - we&amp;rsquo;re saying to execute this against all of the nodes in the &lt;code&gt;hosts&lt;/code&gt; file. Later on we&amp;rsquo;ll see how to separate the nodes into groups inside the inventory file and this will make more sense.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u ian&lt;/code&gt; - the ssh user name for each node&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-a &amp;quot;hostname&amp;quot;&lt;/code&gt; - the command to run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What Ansible has actually done here is ssh into each node and use python to execute the command. Collected the output, then formatted that for us to see. Here it is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;192.168.100.37 | CHANGED | rc=0 &amp;gt;&amp;gt;
vm321-deb
192.168.100.38 | CHANGED | rc=0 &amp;gt;&amp;gt;
vm322-deb
192.168.100.39 | CHANGED | rc=0 &amp;gt;&amp;gt;
vm323-deb
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There&amp;rsquo;s our node IP addresses. The &lt;code&gt;rc=0&lt;/code&gt; is the successful return code, then there&amp;rsquo;s the actual host names - &lt;code&gt;vm321-deb&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;But, what&amp;rsquo;s going on with &lt;code&gt;CHANGED&lt;/code&gt;? Ansible always indicates some sort of status - things like &lt;code&gt;CHANGED&lt;/code&gt;, &lt;code&gt;SUCCESS&lt;/code&gt;, &lt;code&gt;FAILED&lt;/code&gt; etc. In this case, there should not have been any change - we were just retrieving the hose names, not altering them. The best answer is just ignore this for now. The long answer is that when we&amp;rsquo;re using &lt;code&gt;-a&lt;/code&gt; to run commands on a node, Ansible&amp;rsquo;s &lt;code&gt;command&lt;/code&gt; module isn&amp;rsquo;t able to tell if there have been changes or not, so it reports &lt;code&gt;CHANGED&lt;/code&gt; as a better safe than sorry approach.&lt;/p&gt;
&lt;p&gt;Even though it&amp;rsquo;s possible to use Ansible to run native commands, when there is an equivalent Ansible module that can carry out the same action, it&amp;rsquo;s always better to use that. The reason is that that module code is smart enough to see if something needs done or not. If it does not need done, it will just return &lt;code&gt;SUCCESS&lt;/code&gt;, if it needs done, it will carry out what&amp;rsquo;s needed and return &lt;code&gt;CHANGED&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="idempotence"&gt;Idempotence&lt;/h3&gt;
&lt;p&gt;Every Ansible tutorial includes this word, which I have never encountered anywhere else. A command is idempotent if the result is the same no matter how many times it is executed. In the case of Ansible, this is because it checks if something is needed before it does it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at an example. If I wanted to create a test directory in the home folder of each of my machines, the Ansible module for this is the file module. I could use this command:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible -i hosts all -u ian -m file -a &amp;#34;path=test state=directory&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;-m&lt;/code&gt; tells Ansible with module to use, and our arguments after the &lt;code&gt;-a&lt;/code&gt; flag tell Ansible that the state we want to achieve is a directory named &lt;code&gt;test&lt;/code&gt;. Let&amp;rsquo;s run that and have a look at the output:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.03.12-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That makes sense, each one is CHANGED because we needed to create the directory. Let&amp;rsquo;s run it again and see what happens.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.03.25-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This time, since the directory is there, there&amp;rsquo;s no need to change it. Ansible checks for the directories existence before it bothers to create it - because it is idempotent.&lt;/p&gt;
&lt;h3 id="ansiblecfg"&gt;ansible.cfg&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m getting a bit sick of this long command. We can move the inventory file name to a config file to save the typing. Create an ansible.cfg file in your working directory like this.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[defaults]
inventory = hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we can eliminate that from our command line input.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.14.25-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d also like to get rid of the &lt;code&gt;-u ian&lt;/code&gt; from each command. That&amp;rsquo;s not stored in the .cfg file. Since it&amp;rsquo;s likely that your nodes will have different user names in a real situation, they can be stored in the inventory file.&lt;/p&gt;
&lt;h3 id="inventory-file"&gt;Inventory file&lt;/h3&gt;
&lt;p&gt;We started off with a very simple inventory file - literally just a list of IP addresses. let&amp;rsquo;s revisit that to add the ssh user, and while we&amp;rsquo;re there, we can group the nodes according to their functions - this will come in handy later.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[web]
192.168.100.37
192.168.100.38

[db]
192.168.100.39

[web:vars]
ansible_ssh_user=ian

[db:vars]
ansible_ssh_user=ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here I&amp;rsquo;ve created two groups for my nodes, a &lt;code&gt;web&lt;/code&gt; group and a &lt;code&gt;db&lt;/code&gt; group. I&amp;rsquo;ve also set the ssh_user for each group. Now that argument can be left out of out commands. So to get the hostnames now, we can just say:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ansible all -a &amp;#34;hostname&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So much neater! Additionally, since our nodes are in groups now, we can specify the group if we don&amp;rsquo;t want to execute the command on all nodes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.38.41-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-03-at-9.38.41-am.png" width="472" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s probably as far as I want to go in this post. We&amp;rsquo;ve got our heads around some early Ansible concepts, learned how to use the Ad-Hoc commands to do things to our nodes, learned a big word that won&amp;rsquo;t ever come up again except in coding interviews, and seen how to set up the ansible.cfg and inventory files.&lt;/p&gt;
&lt;p&gt;The real power to be unleashed is using Ansible playbooks. We&amp;rsquo;ll look at them next.&lt;/p&gt;</description></item><item><title>How to recover a docker run command</title><link>https://blog.iankulin.com/how-to-recover-a-docker-run-command/</link><pubDate>Sun, 16 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-recover-a-docker-run-command/</guid><description>&lt;p&gt;Imagine if, lets say hypothetically, you&amp;rsquo;d set up an application months ago with a &lt;code&gt;docker run&lt;/code&gt; command. Then you&amp;rsquo;d heard there had been an update to the app because of a security update. So you need to stop/remove the container, pull a new image and restart it, trouble is, you don&amp;rsquo;t remember the exact &lt;code&gt;run&lt;/code&gt; command you used to start it.&lt;/p&gt;
&lt;p&gt;This didn&amp;rsquo;t happen to me, since all my vm setups are in git as markdown (I&amp;rsquo;m pre-Ansible), but I did google how to do this thinking that there would be an easy way before I bothered to look through my config files.&lt;/p&gt;
&lt;h3 id="short-answer"&gt;Short answer&lt;/h3&gt;
&lt;p&gt;There isn&amp;rsquo;t a docker command that will retrieve your run command for you.&lt;/p&gt;
&lt;h3 id="long-answer"&gt;Long answer&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s probably still in your bash history, try:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;history | grep &amp;#34;docker run&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If that doesn&amp;rsquo;t work, it must have been a long time ago.&lt;/p&gt;
&lt;p&gt;Most likely, the crucial information you want to know will be the ports you specified, the network setup, and any directories you&amp;rsquo;ve bound or volumes used. All of this information is available from the &lt;code&gt;docker inspect&lt;/code&gt; command, but you&amp;rsquo;re going to have to trawl through it a bit. Search for &lt;code&gt;Mounts&lt;/code&gt; to see what you did there:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;#34;Mounts&amp;#34;: [
 {
 &amp;#34;Type&amp;#34;: &amp;#34;volume&amp;#34;,
 &amp;#34;Name&amp;#34;: &amp;#34;jellyfin-config&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/var/lib/docker/volumes/jellyfin-config/_data&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/config&amp;#34;,
 &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;z&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;&amp;#34;
 },
 {
 &amp;#34;Type&amp;#34;: &amp;#34;bind&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/mnt/media/video&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/media&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;rprivate&amp;#34;
 },
 {
 &amp;#34;Type&amp;#34;: &amp;#34;volume&amp;#34;,
 &amp;#34;Name&amp;#34;: &amp;#34;jellyfin-cache&amp;#34;,
 &amp;#34;Source&amp;#34;: &amp;#34;/var/lib/docker/volumes/jellyfin-cache/_data&amp;#34;,
 &amp;#34;Destination&amp;#34;: &amp;#34;/cache&amp;#34;,
 &amp;#34;Driver&amp;#34;: &amp;#34;local&amp;#34;,
 &amp;#34;Mode&amp;#34;: &amp;#34;z&amp;#34;,
 &amp;#34;RW&amp;#34;: true,
 &amp;#34;Propagation&amp;#34;: &amp;#34;&amp;#34;
 }
 ],
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="better-answer"&gt;Better Answer&lt;/h3&gt;
&lt;p&gt;Investigate &lt;code&gt;[docker compose](https://docs.docker.com/compose/compose-file/)&lt;/code&gt; to save some effort next time.&lt;/p&gt;</description></item><item><title>Updating SSL Certificates</title><link>https://blog.iankulin.com/updating-ssl-certificates/</link><pubDate>Wed, 12 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/updating-ssl-certificates/</guid><description>&lt;p&gt;When I first installed my SSL certificates, &lt;a href="https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/"&gt;I mentioned&lt;/a&gt; it&amp;rsquo;s a process I need to automate before they came up for expiry, but here we are ten days out, and I haven&amp;rsquo;t done that yet, but I have been keeping an eye on it though the excellent display and notifications set up in &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.36.01-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.36.01-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Updating the certificates is easy. When I went into the site at PorkBun (where I purchased the domain and who do the primary DNS for the site, the next certificates were sitting there to be downloaded. My existing certificates were due to expire on 30th July, and these had been generated on 3rd July.&lt;/p&gt;
&lt;p&gt;The bundle included the same files as last time. You might remember from last &lt;a href="https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/"&gt;time&lt;/a&gt; that we need to join the &lt;code&gt;domain.cert.pem&lt;/code&gt; and &lt;code&gt;intermediate.cert.pem&lt;/code&gt; to make the &lt;code&gt;fullchain.pem&lt;/code&gt; file. I had just &lt;code&gt;cat&lt;/code&gt;&amp;rsquo;d them together and this had caused an issue as there&amp;rsquo;s no newline character at the end of the first file. I got smarter this time and googled up this &lt;a href="https://stackoverflow.com/questions/8183191/concatenating-files-and-insert-new-line-in-between-files/23549826#23549826"&gt;solution&lt;/a&gt; which did the trick by using echo to insert the newline:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.57.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-5.57.44-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once that was done, I uploaded them to the nginx directory where I stored them last time. Nginx reloads the config on restart, although there&amp;rsquo;s probably a neater way as well, so I just restarted the container with Docker compose to pick up the new certificates. While I was doing that I got the ping from Uptime Kuma via &lt;a href="https://ntfy.sh/"&gt;ntfy&lt;/a&gt; to say it was down, then up. I had a look at the display, and it&amp;rsquo;s showing I&amp;rsquo;ve got another 84 days left on the cert.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-6.10.32-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-10-at-6.10.32-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, 84 days for me to get around to automating this.&lt;/p&gt;</description></item><item><title>How to deploy a Node.js app</title><link>https://blog.iankulin.com/how-to-deploy-a-node-js-app/</link><pubDate>Wed, 05 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-deploy-a-node-js-app/</guid><description>&lt;p&gt;This is one of those things that is simple once you know it. I had my &lt;a href="https://blog.iankulin.com/using-node-js-to-return-a-static-file/"&gt;tiny Node service working&lt;/a&gt; on my MacBook, but how do I run it on the server?&lt;/p&gt;
&lt;h3 id="native-or-container"&gt;Native or Container&lt;/h3&gt;
&lt;p&gt;Obviously I need Node.js installed on the server, should I have it in a Docker container, or native on the machine. There&amp;rsquo;s no clear answer here - in a container set up with Docker Compose might be more in line with my ideology of treating machines as disposable, but a native install is simpler, and I probably want to make life simpler at this stage when I&amp;rsquo;m learning everything.&lt;/p&gt;
&lt;h3 id="installing-node"&gt;Installing Node&lt;/h3&gt;
&lt;p&gt;This took me down a bigger rabbit hole than I was expecting. My VPS is Unbuntu LTS 22.04.2, so I spun one of those up in a VM on the homelab to try things out.&lt;/p&gt;
&lt;p&gt;A quick google search suggested the &lt;a href="https://github.com/nodesource/distributions"&gt;NodeSource binary distributions&lt;/a&gt;. That involves curling a big script (when I pasted the script into ChatGPT it said it wasn&amp;rsquo;t malicious). I could chose the Node version, so I grabbed 20.x That was as painless as you&amp;rsquo;d expect.&lt;/p&gt;
&lt;p&gt;Then I started wondering why I couldn&amp;rsquo;t just &lt;code&gt;apt install&lt;/code&gt; it. If I could do that, it would reduce the chance of a supply chain attack since I&amp;rsquo;d have the power of Canonical on my side. So I rolled the previous install back (thank you Proxmox backups of VMs), and tried:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt install nodejs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That worked fine - Node is in the Ubuntu packages, but the version is &lt;a href="https://nodejs.dev/en/about/releases/"&gt;quite old&lt;/a&gt; - v12.22.9. This is on the current Ubuntu LTS 22.04.2. I don&amp;rsquo;t think it will matter for my purposes, but it explains why you&amp;rsquo;d do something other than just this.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also going to need &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt;, so lets get that with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt install npm&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That seemed to download a heap more stuff that the node install.&lt;/p&gt;
&lt;h3 id="deploying-your-project"&gt;Deploying your project&lt;/h3&gt;
&lt;p&gt;Again, the first search result was more complicated than I needed. The advice was to clone my repository onto the server where I wanted to deploy. This is such a minor project, I hadn&amp;rsquo;t pushed it up to GitHub. So that seemed excessive. You know, not everything has to be DevOps CI/CD! I mean, we ain&amp;rsquo;t talking about a very complicated project here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-26-at-8.34.20-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got this tiny source file, and the text file I want to serve. All the dependencies (just Express) are in the &lt;code&gt;package.json&lt;/code&gt;, so presumably that&amp;rsquo;s all I need on the server to get going.&lt;/p&gt;
&lt;p&gt;I &lt;code&gt;scp&lt;/code&gt;&amp;rsquo;d those from my laptop to a directory on the folder:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-26-at-8.41.19-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once they are there, I need to install the packages from the package.json, so we do that with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That installed 59 packages (presumably Express plus 58 of it&amp;rsquo;s dependencies). Then I started the app with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;node .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and it worked!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-06-26-at-8.55.34-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="insomnia"&gt;Insomnia&lt;/h3&gt;
&lt;p&gt;I should probably explain what you&amp;rsquo;re looking at above. I could have tested this little node server by going to the api address in a browser and checked that I got back the text file I was expecting. And in Chrome (and I assume Firefox) there are developer tools that would show the return code etc. However, most of the REST API videos I&amp;rsquo;ve watched use a better tool - mostly &lt;a href="https://www.postman.com/"&gt;Postman&lt;/a&gt;. These sort of tools give you a heap of other capabilities, none of which I really need for this simple project, but will be very handy for more complex APIs where there is a body to the request.&lt;/p&gt;
&lt;p&gt;The only reason I&amp;rsquo;m using &lt;a href="https://insomnia.rest/"&gt;Insomnia&lt;/a&gt; instead of Postman is that when I tried Postman, it straightaway wanted some of my data to make it work. Insomnia hasn&amp;rsquo;t forced me to do that yet.&lt;/p&gt;</description></item><item><title>Containers</title><link>https://blog.iankulin.com/containers/</link><pubDate>Sun, 07 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/containers/</guid><description>&lt;p&gt;There&amp;rsquo;s a few things that really strike me as significant improvements to life since I was commercially developing 20 years ago:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accessing information - the first time I &lt;em&gt;bought&lt;/em&gt; the development stack to write commercial software against the Windows SDK it came in a huge carton with, I guess, fifteen or so 2&amp;quot; thick books. That was how you looked things up in those days. Fast forward to an internet connected world of websites, stack exchange, Discord and ChatGPT. So much better.&lt;/li&gt;
&lt;li&gt;Open Source - is an actual useful thing that the entire connected world runs on - not just a weird hippy idea. It&amp;rsquo;s almost routine to open source your code now and everyone benefits from that.&lt;/li&gt;
&lt;li&gt;Containers - &amp;ldquo;getting things working&amp;rdquo; used to be a thing. Most times now I want to spin something up to play with it, it just works because all the dependencies are bundled with it, and it doesn&amp;rsquo;t mutate the environment in any way I don&amp;rsquo;t know about. There&amp;rsquo;s no friction to run a giant app, and no hangover for the OS when I nuke it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I love this great explanation from Coderized about containers - I wish I&amp;rsquo;d seen it five months ago.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/J0NuOlA2xDc?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Git/GutHub - macOS - marking file as executable</title><link>https://blog.iankulin.com/git-guthub-macos-marking-file-as-executable/</link><pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-guthub-macos-marking-file-as-executable/</guid><description>&lt;p&gt;I&amp;rsquo;m working on the world&amp;rsquo;s shortest shell script - it&amp;rsquo;s called by &lt;code&gt;cron&lt;/code&gt; to pull down a JSON weather report to a text file using &lt;code&gt;curl&lt;/code&gt; so I can expose it on an Nginx endpoint. The purpose is to allow me to hammer that weather API from multiple machines I control without violating the TOS of my free API key.&lt;/p&gt;
&lt;p&gt;Because I&amp;rsquo;m learning all the things, instead of just creating this on the VPS where it runs, it&amp;rsquo;s cloned from my GitHub repo for that machine. I&amp;rsquo;m creating and editing the file in VS Code on macOS, pushing to Github, then pulling the changes on the Ubuntu VPS. The intention is that this will eventually become automated with a Github action.&lt;/p&gt;
&lt;p&gt;The problem I&amp;rsquo;ve run into is that I want the file permissions so show the file is executable so when it arrives on the VPS - so no &lt;code&gt;chmod&lt;/code&gt; is required to make it usable.&lt;/p&gt;
&lt;p&gt;Some googling suggested that the executable flag (but none of the other file permissions) is stored and handled by git, and furthermore, there&amp;rsquo;s a git command to set it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git update-index --chmod=+x bin/fetchWeather.sh 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I wrote my (one line) script, applied the command above, committed and pushed, then pulled it down on the VPS and the bit wasn&amp;rsquo;t set. So somewhere in this chain there&amp;rsquo;s a problem.&lt;/p&gt;
&lt;p&gt;At this stage, it&amp;rsquo;s helpful to know that if the executable bit is set for a file, GitHub shows this in the header of the file where it says how many lines etc.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.26.25-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.26.41-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In my case, it was showing that the file was not marked as executable in GitHub, so the problem was that the &lt;code&gt;git update-index&lt;/code&gt; was not working for me for some reason.&lt;/p&gt;
&lt;p&gt;A bit more investigation turned up that there&amp;rsquo;s a setting in the &lt;code&gt;.git/config&lt;/code&gt; file called &lt;code&gt;filemode&lt;/code&gt; that controls if the originating file system executable status is preserved. That sounded promising - I was expecting to find that is was set to false, and I could change it to true, and it would fix my problem. I had a quick look and, oh, it&amp;rsquo;s already set to true.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.36.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.36.54-pm.png" width="656" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Seems like it&amp;rsquo;s involved though, so perhaps (my thinking went) I should change it to false and see if the problem goes away&amp;hellip;. and it did. I changed this value to &lt;code&gt;false&lt;/code&gt;, applied the executable bit with the &lt;code&gt;git update-index&lt;/code&gt; command, committed, pushed it to GitHub (it was marked executable), pulled it down to the VPS, it was still marked executable!&lt;/p&gt;
&lt;p&gt;My whole tech life, I&amp;rsquo;ve never been happy with solutions to problems where I don&amp;rsquo;t understand the underlying reasons. If things just start working when you&amp;rsquo;re fiddling around and you&amp;rsquo;re not clear on why, it feels like they could change back with just as easily and with no more reason.&lt;/p&gt;
&lt;p&gt;A clue to what&amp;rsquo;s going on (many readers will already have figured this out) was given to me by ChatGPT. When I was asking it about this issue, it kept insisting I should &lt;code&gt;chmod&lt;/code&gt; the file to be executable before I committed it. I had to be really clear with it that this wasn&amp;rsquo;t possible on macOS because it doesn&amp;rsquo;t have that sort of file permissions&amp;hellip;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/cain.jpg" width="140" alt=""&gt;
&lt;p&gt;Of course, in fact, it does. &lt;a href="https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/BSD/BSD.html"&gt;macOS is based on FreeBSD&lt;/a&gt; (&amp;ldquo;without the good bits&amp;rdquo; goes the old joke told at Unix conferences). I&amp;rsquo;d just somehow forgotten this - I guess in Linux I&amp;rsquo;m used to explicitly seeing them every time I look at a directory contents, but never see it on Mac. Even if you go into &amp;ldquo;Get Info&amp;rdquo; for a file in Finder on the mac, you can see the read/write permissions, but not the executable bit status.&lt;/p&gt;
&lt;p&gt;So how do you set and view the executable status on mac? Exactly the same as on any Unix.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.52.17-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.52.17-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I did that, and changed the /&lt;code&gt;git/config filemode&lt;/code&gt; back to &lt;code&gt;true&lt;/code&gt;. Committed and pushed the file up (without worrying about the &lt;code&gt;git update-index&lt;/code&gt;) and it showed up in GitHub as executable, pulled it down, still executable.&lt;/p&gt;</description></item><item><title>Installing SSL Certificates with Nginx on Docker</title><link>https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/</link><pubDate>Sat, 29 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-ssl-certificates-with-nginx-on-docker/</guid><description>&lt;p&gt;When you&amp;rsquo;ve successfully got Nginx running in a Docker container, AND got your &lt;a href="https://blog.iankulin.com/adding-a-domain-name-to-a-vps/"&gt;domain correctly pointing&lt;/a&gt; at your nascent website, you&amp;rsquo;re then going to want to set it up for encrypted, and therefore trusted, browsing with SSL.&lt;/p&gt;
&lt;h3 id="certificates"&gt;Certificates&lt;/h3&gt;
&lt;p&gt;A couple of posts ago, I &lt;a href="https://blog.iankulin.com/adding-a-domain-name-to-a-vps/"&gt;mentioned&lt;/a&gt; that it was simpler to let Porkbun be the authoritative nameserver for a domain. Part of the reason for that is that if we do that, Porkbun had a button you can press which connects to LetsEncrypt and generates the certificates for you. This usually takes an hour or so, then you&amp;rsquo;ll be able to download the bundle from that same page.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-2.30.58-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-2.30.58-pm.png" width="913" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In order for the SSL to work, we&amp;rsquo;re going to have to make a couple of files available to Nginx - &lt;code&gt;fullchain.pem&lt;/code&gt; and &lt;code&gt;private.key.pem&lt;/code&gt;. So there&amp;rsquo;s our first gotcha - we don&amp;rsquo;t have a &lt;code&gt;fullchain.pem&lt;/code&gt;, so we have to build it. To do this, we just combine the domain certificate and the intermediate certificate. On the mac, I did this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cat domain.cert.pem intermediate.cert.pem &amp;gt; fullchain.pem
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is me solving the first gotcha, while simultaneously creating the second. Much later in the process when Nginx was failing at startup, I looked in the logs (with the handy &lt;code&gt;docker logs&lt;/code&gt; command) and saw these messages:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;2023/04/21 05:42:45 [emerg] 1#1: cannot load certificate &amp;#34;/etc/nginx/conf.d/fullchain.pem&amp;#34;: PEM_read_bio_X509() failed (SSL: error:0908F066:PEM routines:get_header_and_data:bad end line)
nginx: [emerg] cannot load certificate &amp;#34;/etc/nginx/conf.d/fullchain.pem&amp;#34;: PEM_read_bio_X509() failed (SSL: error:0908F066:PEM routines:get_header_and_data:bad end line)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s a reasonably descriptive error - let&amp;rsquo;s look in the &lt;code&gt;fullchain.pem&lt;/code&gt; file (it&amp;rsquo;s just text like an SSH key file) and see if there&amp;rsquo;s anything suspicious.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-1.46.32-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well there&amp;rsquo;s a problem. These beginning and ends should be on their own lines - I probably could have done that when I concatenated them, but no problem, it&amp;rsquo;s easily fixed in the text editor by counting in five dashes and hitting enter.&lt;/p&gt;
&lt;h3 id="nginx-docker"&gt;Nginx Docker&lt;/h3&gt;
&lt;p&gt;In order to have the certificates work with Nginx, we&amp;rsquo;re going to need to add them to the a config file. There&amp;rsquo;s also a couple of gotcha&amp;rsquo;s in that process.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re as new to running Nginx in a container as I am, you might have been starting it up with a command like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;docker run -p 80:80 -d -v ~/www:/usr/share/nginx/html nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s fine and all, but as your system gets a bit more complex (which it&amp;rsquo;s about to) this will quickly become unmanageable. It&amp;rsquo;s time to put your big person pants on and embrace the wonders of &lt;code&gt;docker compose&lt;/code&gt;. There are many resources for learning this, but the short version is that all of that information you&amp;rsquo;ve got in your command line can be stored in a human readable YAML file. If you&amp;rsquo;re smart it will also be in version control and you&amp;rsquo;re on your journey to automating your infrastructure as code.&lt;/p&gt;
&lt;p&gt;Below is my &lt;code&gt;docker-compose.yaml&lt;/code&gt; file for Nginx. Note that these files are always called that, so you keep the compose files for different containers in separate directories.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;version: &amp;#34;3.9&amp;#34;

services:
 client:
 image: nginx
 container_name: nginx
 ports:
 - 80:80
 - 443:443
 volumes:
 - /home/ian/iankulin.com/www:/usr/share/nginx/html
 - /home/ian/iankulin.com/nginx/conf/:/etc/nginx/conf.d/:ro
 restart: always
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;version&lt;/code&gt; - just the compose yaml version docker should use to read this file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;services/client&lt;/code&gt; - it&amp;rsquo;s possible to combine several programs (clients) in a docker container. We&amp;rsquo;re not doing that today&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt; - the name of the docker image we&amp;rsquo;re pulling down from &lt;a href="https://hub.docker.com/"&gt;docker hub&lt;/a&gt;. We could also add the version here if we were picky, if one&amp;rsquo;s not specified, it assumes &amp;rsquo;latest'&lt;/li&gt;
&lt;li&gt;&lt;code&gt;container_name&lt;/code&gt; - nice name for our container - it&amp;rsquo;s possible to run several versions of the same image so you may want to name them something different. If you miss this off, docker will make up a default name like &lt;code&gt;agressive_einstein&lt;/code&gt; and you&amp;rsquo;ll constantly be running &lt;code&gt;docker ps&lt;/code&gt; because you can&amp;rsquo;t remember the name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ports&lt;/code&gt; - the underlying idea of containers is that they are mostly immutable inside, but of course to be useful they need to have some access to the outside world. This ports declaration is doing just that. These two lines are saying port 80 outside the container is connected to port 80 inside the container. If we wanted Nginx running on port 8080 we&amp;rsquo;d say &lt;code&gt;8080:80&lt;/code&gt; ie the outside first, and the inside second&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volumes&lt;/code&gt; - similar to ports, we&amp;rsquo;re joining a directory of our file system in outside world to a directory inside the container. In the first one, Nginx is going to look for files to serve inside the container at &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; but where we want it to look for html files to serve is actually out here in the real filesystem world. Same as with the ports, the format is real world first, inside the container second. Same with the config directory. You might be wondering how I know what the paths inside the container are for these things - you just have to figure it out from the &lt;a href="https://hub.docker.com/_/nginx"&gt;documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;So we&amp;rsquo;ve got two outside world locations - our html files in &lt;code&gt;/home/ian/iankulin.com/www&lt;/code&gt; and the Nginx config files in &lt;code&gt;/home/ian/iankulin.com/nginx/conf/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the config file. This is stored in &lt;code&gt;/home/ian/iankulin.com/nginx/conf/&lt;/code&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
 listen 80 default_server;
 listen [::]:80 default_server;
 root /usr/share/nginx/html;
 server_name iankulin.com www.iankulin.com;

 listen 443 ssl; 

 # RSA certificate
 ssl_certificate /etc/nginx/conf.d/fullchain.pem; 
 ssl_certificate_key /etc/nginx/conf.d/private.key.pem; 
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is mostly pretty decodable just by looking at it, but there&amp;rsquo;s a couple of things worth noting.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the root for the html, like all of the paths in this file, are the paths &lt;em&gt;inside&lt;/em&gt; the container. Don&amp;rsquo;t get confused. To the programs running inside the container, everything looks like it&amp;rsquo;s inside the container. This config file is being consumed by the Nginx program inside the container, so the paths have to be inside-the-container paths.&lt;/li&gt;
&lt;li&gt;Following that logic, I&amp;rsquo;ve actually stored the SSL certificates at &lt;code&gt;/home/ian/iankulin.com/nginx/conf/&lt;/code&gt; but to Nginx inside the container, they look like they&amp;rsquo;re at &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;There is &lt;em&gt;way&lt;/em&gt; more stuff you can do in this config file. This is just the simplest version possible to make things work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So now that&amp;rsquo;s in place, and I&amp;rsquo;ve got a skeleton of an index.html file stored at &lt;code&gt;/home/ian/iankulin.com/www&lt;/code&gt; I just enter &lt;code&gt;sudo docker compose up -d&lt;/code&gt; in the directory where my &lt;code&gt;docker-compose.yaml&lt;/code&gt; file is, and I should be able to navigate to &lt;code&gt;http**s**://iankulin.com&lt;/code&gt; and get a webpage with a padlock in the corner.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-1.48.00-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-1.48.00-pm.png" width="859" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="success"&gt;Success&lt;/h3&gt;
&lt;p&gt;Well of sorts. We have obtained our certificates, and installed them in the webserver, but certificates like these only last 90 days. In 75 days I can obtain new certificates and copy them over the old ones. If we fail to do that by the 90th day, visitors to the website will get a scary message saying the website might not be who it says it is, and users will have to click around a bit to ignore it. You will have almost certainly seen this message as it&amp;rsquo;s a reasonably common problem.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a problem calling out for an automated solution, of which there &lt;a href="https://certbot.eff.org/"&gt;is one&lt;/a&gt; that we&amp;rsquo;ll install on another day. Probably the day I come back to this server and discover the certificates have expired&amp;hellip;&lt;/p&gt;</description></item><item><title>Adding a Domain Name to a VPS</title><link>https://blog.iankulin.com/adding-a-domain-name-to-a-vps/</link><pubDate>Fri, 28 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/adding-a-domain-name-to-a-vps/</guid><description>&lt;p&gt;I&amp;rsquo;ve had a small &lt;a href="https://www.binarylane.com.au/"&gt;BinaryLane VPS&lt;/a&gt; for a while that I use for homelab type stuff, but now need to serve a tiny amount of JSON from it. A longer term plan is to use it as a &lt;a href="https://www.wireguard.com/"&gt;Wireguard&lt;/a&gt; tunnel back to my cluster at home to expose the services that need to be internet facing. I&amp;rsquo;ve also had a domain name I bought from &lt;a href="https://porkbun.com/products/domains"&gt;Porkbun&lt;/a&gt; sitting round for a bit, so it&amp;rsquo;s probably a good time to join them up.&lt;/p&gt;
&lt;p&gt;When you type a domain name into your web browser it needs to be turned into an IP address in order to return the content you need from that web server. For example if I type in &lt;code&gt;google.com&lt;/code&gt; it needs to be turned into &lt;code&gt;172.217.24.46&lt;/code&gt; in order to fetch the front page of Google.&lt;/p&gt;
&lt;p&gt;The things that provide this translation service are the Domain Name Service (DNS). There&amp;rsquo;s several layers of DNS and if the first layer asked does not know, the request gets escalated until it is found or the request fails - but somewhere in that chain there needs to be a name server (the Authoritative DNS) that knows the domain name and the IP address. DNS is cached all over the place, so most requests don&amp;rsquo;t get all the way back there (and changes sometimes take a little while to percolate around) but there must an an authoritative name server somewhere.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have my domain pointed to the BinaryLane name servers, but it&amp;rsquo;s currently pointed to the Porkbun name servers, and it&amp;rsquo;s simpler for me to leave it there.&lt;/p&gt;
&lt;p&gt;All I need to do at the Porkbun end is go into Domain Management, and open up the &amp;ldquo;DNS entries&amp;rdquo; for the domain, and edit the &amp;ldquo;A Records&amp;rdquo; to point at the IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-9.24.12-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s enough that a few minutes later, typing the domain address into a web browser pulls up the test page from the Nginx web server running in a container on my VPS.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-9.35.48-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-21-at-9.35.48-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Using NAS for Proxmox backups</title><link>https://blog.iankulin.com/using-nas-for-proxmox-backups/</link><pubDate>Mon, 10 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-nas-for-proxmox-backups/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/"&gt;A few weeks ago&lt;/a&gt;, I was very excited to be able to take a snapshot of a virtual machine, copy it across the network from that Proxmox node, copy it back across the network to a different Proxmox node, start it there, and have it up and running, without it noticing it was actually on different hardware.&lt;/p&gt;
&lt;p&gt;Backing up a VM is pretty simple, you just click on the node, choose &lt;em&gt;Backup&lt;/em&gt; and click the &lt;em&gt;Backup Now&lt;/em&gt; button. The ease, and completeness of backing up a VM is one of the main reasons I&amp;rsquo;m using Proxmox for my systems.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-12.02.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-12.02.59-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By default, VM backups are saved to the &amp;ldquo;local drive&amp;rdquo; - actually the &lt;code&gt;/var/lib/vz&lt;/code&gt; directory. This would not be useful if the physical machine dies, but also it&amp;rsquo;s not convenient to restore to a different machine. Ideally you&amp;rsquo;d have a central place to store these files that was accessible to all the Proxmox nodes.&lt;/p&gt;
&lt;p&gt;This is exactly the situation I&amp;rsquo;ve setup with my lab, the NAS is the storage for the VM backups. Each of the Proxmox nodes uses the same directory for backups, so moving a machine from one node to another is a simple as backing it up on one node, stopping the VM, and restoring it on another node just by choosing the backup file to restore in the web GUI.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;Proxmox can use all sorts of shares as a location for backups (and other files such as the ISO&amp;rsquo;s used to boot new machines), but the simplest is probably &lt;a href="https://en.wikipedia.org/wiki/Network_File_System"&gt;NFS&lt;/a&gt;. This is also straightforward to do from the Synology NAS.&lt;/p&gt;
&lt;p&gt;In the web interface for the NAS, go into &lt;em&gt;Control Panel&lt;/em&gt;, &lt;em&gt;Shared Folder&lt;/em&gt; and create a new shared folder. I called mine Proxmox. One of the tabs there is for NFS permissions - just add the IP address of the Proxmox node that you&amp;rsquo;d life to access the folder.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-1.46.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not much harder from the Proxmox end. Although the storage you add will appear at the node level in the &lt;em&gt;Server View&lt;/em&gt; of the web GUI, it is added at the &lt;em&gt;Datacenter&lt;/em&gt; level.&lt;/p&gt;
&lt;p&gt;Go into &lt;em&gt;Storage&lt;/em&gt;, select &lt;em&gt;Add&lt;/em&gt; and choose &lt;em&gt;NFS&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.00.04-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.00.04-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then enter an ID (this will be the name of the storage in Proxmox) and the IP address. If you wait half a second, then you can click the dropdown for all the folders that are shared from that IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.06.19-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The last field is content - this refers the the type of Proxmox stuff you want to keep in there - for backups, you just need VZDumps, but I usually click on everything since I&amp;rsquo;ll also use it for ISOs for new VMs and templates for LXCs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.11.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve added that, the storage will appear in the server view, but also as an option when you go into &lt;em&gt;Backup&lt;/em&gt; for a VM and select &lt;em&gt;Backup Now&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.15.53-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Proxmox VM Memory Upgrade</title><link>https://blog.iankulin.com/proxmox-vm-memory-upgrade/</link><pubDate>Sun, 19 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-vm-memory-upgrade/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.36.10-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I ordered some RAM this week for my production server - it&amp;rsquo;s quickly becoming clear that memory is the limiting factor when running lots of services and VM&amp;rsquo;s that don&amp;rsquo;t get much use - rather than processing power. I&amp;rsquo;m not really a hardware guy, so figuring out exactly what RAM I need is a slightly fraught process - I won&amp;rsquo;t be fully confident I&amp;rsquo;ve ordered the right thing until I install it, boot up, and see my &lt;a href="https://support.hp.com/us-en/product/hp-elitedesk-800-35w-g2-desktop-mini-pc/7633266/document/c04816235"&gt;G2 800&lt;/a&gt; come to life maxed out at 32GB.&lt;/p&gt;
&lt;p&gt;Something that&amp;rsquo;s not fraught however, is upgrading the RAM in a virtual machine (VM) running under &lt;a href="https://www.proxmox.com/en/proxmox-ve"&gt;Proxmox&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="ram-hunger"&gt;RAM Hunger&lt;/h3&gt;
&lt;p&gt;I run two VM&amp;rsquo;s full time on the production node - a general docker host for a variety of small services, and a separate VM for &lt;a href="https://jellyfin.org/"&gt;Jellyfin&lt;/a&gt;. I&amp;rsquo;d allocated 6GB for this VM, but when I checked tonight ProxMox was reporting that 5GB was already being used.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.16.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.16.57-pm.png" width="974" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have noticed that the Jellyfin memory usage seems to slowly grow over time. That might be related to my current usage pattern - I&amp;rsquo;m frequently re-scanning the libraries as I check and update the metadata.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.17.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In any case, it needs more RAM, and I&amp;rsquo;ve got some up my sleeve on this physical machine so let&amp;rsquo;s allocate some more to the Jellyfin VM.&lt;/p&gt;
&lt;p&gt;Normally, you specify the amount of RAM to allocate when you&amp;rsquo;re creating the machine, but it&amp;rsquo;s quite straightforward to change it afterwards. With your VM selected, click into the &amp;ldquo;Hardware&amp;rdquo; page. Then if you double click on &amp;ldquo;Memory&amp;rdquo; a dialogue will open up to&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.18.19-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can just edit this number, in MB. Once you OK it, there will be two values listed for memory in the Hardware specs. The first is what the VM is running with now, and the second, orange value is what you are changing it to. In my case, I&amp;rsquo;ve bumped it up to 8GB from 6.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.19.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not possible to change the memory dynamically - it requires a reboot. Of course, rebooting the machine also restarts Jellyfin, so after the reboot we have plenty of headroom.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-16-at-6.58.21-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Accessing a Synology NAS from Linux</title><link>https://blog.iankulin.com/accessing-a-synology-nas-from-linux/</link><pubDate>Mon, 20 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/accessing-a-synology-nas-from-linux/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_4154x.jpg" width="1000" alt=""&gt;
&lt;p&gt;I picked up a Synology DS216j NAS from eBay to use for storage for the rapidly growing home lab. The eventual plan is that as well as my VM backups, it will host the media library, and eventually (when this has all proved itself reasonably bullet-proof) my current DropBox contents. That won&amp;rsquo;t all fit on the 2x2TB drives that the DS216j came with, and I have a pair of 8TBs on hand, but I wanted to set it up and checked it all worked.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.15.25-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Configuration of the NAS was a &amp;lsquo;follow the prompts&amp;rsquo; exercise for the most part. The Synology OS is a Linux port called DSM, but it&amp;rsquo;s intended to be an appliance so all the interactions are through the web client. I&amp;rsquo;m using RAID 1 since the plan is that the production segment of the homelab will all be high-ish available. There&amp;rsquo;s a few options to install extras (such as Tailscale), but these little &amp;lsquo;j&amp;rsquo; models don&amp;rsquo;t run an x86 processor, so no docker etc.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d got through all of that, I created a share in &amp;lsquo;File Station&amp;rsquo; and copied a couple of files in. By default, Samba shares are on (with the name WORKGROUP - so I guess this is aimed at making it simple for Windows users) but NFS are not. I know nothing about NFS, so this suits me for the moment. Additionally, my &lt;a href="https://en.wikipedia.org/wiki/WD_TV"&gt;WD-TV&lt;/a&gt; shares it&amp;rsquo;s attached USB drive using Samba, so I&amp;rsquo;m used to accessing it from the MacBook. Let&amp;rsquo;s try the NAS from the MacBook:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.23.38-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.23.38-pm.png" width="512" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It asked for the login details, then I was in. Could not have been much easier.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.28.55-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-3.28.55-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="accessing-from-linux"&gt;Accessing from Linux&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m planning on running Jellyfin in an LCX container. So I&amp;rsquo;ll set that up for this test too. I stood it up with the Debian server .iso in Proxmox and specified it should be a &amp;lsquo;privileged&amp;rsquo; container, and in the Proxmox options for the LXC ticked &amp;lsquo;SMB/CIFS&amp;rsquo;. This process is not just for Synology - it will work to mount any samba share on a network to your Linux machine.&lt;/p&gt;
&lt;p&gt;We need to make an empty directory to mount to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir /mnt/media
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then edit (I use nano) the file &lt;code&gt;/etc/fstab&lt;/code&gt; to include:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,uid=1000,gid=1000,file_mode=0660,dir_mode=07
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;etc/fstab&lt;/code&gt; runs at startup, and if the shares are available (cautionary note about booting up your lab after a power outage) it will set them up. There&amp;rsquo;s a fair bit going on in the command, perhaps we should pull it apart:&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;//192.168.100.25/media /mnt/media&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The first directory is the share, the second is the empty directory on this machine we are mounting the share to.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;username=jelly, password=jellypass&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Our credentials needed to log into the share. Actually I used my root credentials, but obviously a good idea would be to make a user on the NAT for this specific purpose with only access to the share they need to operate.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;uid=1000, gid=1000&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Okay, we're about to enter the Linux zone... These numbers are a &lt;a href="https://medium.com/@gggauravgandhi/uid-user-identifier-and-gid-group-identifier-in-linux-121ea68bf510"&gt;Linux user id and a group ownership id&lt;/a&gt; that Linux assigns to resources - you know, for &lt;code&gt;chown&lt;/code&gt; and stuff like that. If you type in &lt;code&gt;id&lt;/code&gt; at the CLI you can see your numbers. For some reason code examples often use 1000 for both, and things seem to work so I don't worry about it.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;file_mode=0660, dir_mode=07&lt;/code&gt;&lt;/td&gt;&lt;td&gt;More Linux permission stuff. Used in combination with the previous two parameters, and &lt;a href="http://file_mode=0660,dir_mode=07"&gt;will probably cause me problems later&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Note that a couple of other posts on the internet about mounting samba shares thought I&amp;rsquo;d have to do one or both of these commands to install extra samba goodness:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt install smbclient
apt install cifs-utils
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But it turns out I didn&amp;rsquo;t. I suspect that was something to do with ticking the box for SMB/CIFS when I was creating the LXC container in Proxmox.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve saved that command in &lt;code&gt;/etc/fstab&lt;/code&gt;, reload the mounts with:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mount -a
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If there&amp;rsquo;s no errors, you are probably right to go. Have a look at your mount point to see your shared files.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-4.59.09-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Since the mount command is in the /etc/fstab file, this mount will be durable - as long as the share is available, it will be mounted every time this machine starts.&lt;/p&gt;</description></item><item><title>Configuring Proxmox for Free Use</title><link>https://blog.iankulin.com/configuring-proxmox-for-free-use/</link><pubDate>Thu, 16 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/configuring-proxmox-for-free-use/</guid><description>&lt;p&gt;I installed Proxmox on my second server last night, and tonight when I ran &lt;code&gt;apt update&lt;/code&gt; I ran into the error you get when you haven&amp;rsquo;t bought a license.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Err:5 https://enterprise.proxmox.com/debian/pve bullseye InRelease 
 401 Unauthorized [IP: 103.67.14.50 443]
Reading package lists... Done 
E: Failed to fetch https://enterprise.proxmox.com/debian/pve/dists/bullseye/InRelease 401 Unauthorized [IP: 103.67.14.50 443]
E: The repository &amp;#39;https://enterprise.proxmox.com/debian/pve bullseye InRelease&amp;#39; is not signed.
N: Updating from such a repository can&amp;#39;t be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even though I guess it was only a month ago (let that sink in people who think the raspberry Pi they just bought is going to be the last homelab hardware they buy 😊) since I set up my first Proxmox server, I&amp;rsquo;d already forgotten there&amp;rsquo;s a step to enable it to get updates without a subscription.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a couple of little steps for this. They are both &lt;a href="https://pve.proxmox.com/wiki/Package_Repositories#sysadmin_enterprise_repo"&gt;here on the Proxmox wiki&lt;/a&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list.d/pve-enterprise.list&lt;/code&gt; to comment out the single repository listed in there.&lt;/li&gt;
&lt;li&gt;edit &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; to look like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;deb http://ftp.debian.org/debian bullseye main contrib
deb http://ftp.debian.org/debian bullseye-updates main contrib

# PVE pve-no-subscription repository provided by proxmox.com,
# NOT recommended for production use
deb http://download.proxmox.com/debian/pve bullseye pve-no-subscription

# security updates
deb http://security.debian.org/debian-security bullseye-security main contrib
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then you&amp;rsquo;ll be good to go.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-07-at-8.41.15-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Moving a VM between two Proxmox hosts</title><link>https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/</link><pubDate>Thu, 16 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/</guid><description>&lt;img src="https://blog.iankulin.com/images/s-l640.jpg" width="264" alt=""&gt;
&lt;p&gt;So, the very small datacentre has undergone a major hardware upgrade today. The HP 800 G1 is joined by an HP 800 G2. Four core i7 vs the old two core i5. Double the RAM to 16GB, four times the disk. The old machine will become a dev/play machine - still virtualised, and the new machine will run the production apps, mostly in Docker containers.&lt;/p&gt;
&lt;p&gt;Since everything is containerised, I did consider running Unbuntu Server on the bare metal of the new machine, but running it on Proxmox will give me some flexibility, and since we&amp;rsquo;ve stepped up the underlying hardware resource so substantially, performance will be well in front anyway. Plus it will give me some flexibility if needed in the future.&lt;/p&gt;
&lt;p&gt;Another massive benefit of virtualisation is the ability to backup a VM to a single file.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve invested several hours in the old server - downloading ISOs, updating everything, installing Docker, adding my containers, reserving the IP addresses in DNS and so on. Wouldn&amp;rsquo;t it be amazing if I could stop my main VM, back it up, copy the backup to the new server, then boot it there and have every thing just work.&lt;/p&gt;
&lt;p&gt;In theory this should be entirely possible. So let&amp;rsquo;s give it a go.&lt;/p&gt;
&lt;p&gt;In the Proxmox web interface, you can execute a backup on a VM. There&amp;rsquo;s three flavours with &lt;code&gt;STOP&lt;/code&gt; being the most reliable as it actually stops the VM to grab it&amp;rsquo;s copy. On this system I can easily afford to stop everything for ten minutes so I&amp;rsquo;ll actually be shutting down my VM and doing this sort of back up. We do this by clicking on the VM, then selecting backup. At the top is a backup button.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-8.35.38-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-8.35.38-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve done your backup it appears in a couple of places in the web interface - in this backup screen associated with the VM, but also if you select the &lt;code&gt;local&lt;/code&gt; disk then backup.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-8.41.43-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s my VM nicely backed up into a single tarball, now I want to download it. I really feel the Proxmox interface should have buttons for Download and Upload on this screen - that would make this operation even easier. But it does not.&lt;/p&gt;
&lt;p&gt;The first problem is to find where these files are stored. Thanks to u/walalauw&amp;rsquo;s answer in &lt;a href="https://old.reddit.com/r/Proxmox/comments/jj6eqz/downloading_backups/"&gt;this reddit thread&lt;/a&gt;, it sounds like they are at &lt;code&gt;/var/lib/vz/dump&lt;/code&gt; I head there in FileZilla, and find:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-7.54.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-7.54.09-pm.png" width="826" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You only need the &lt;code&gt;.zst&lt;/code&gt; file, but neat freaks can grab the the &lt;code&gt;.notes&lt;/code&gt; as well. It contains the text you wrote for the backup - in the previous screenshot you can see I&amp;rsquo;d written &amp;ldquo;Ready to move&amp;rdquo; for this one.&lt;/p&gt;
&lt;p&gt;Copy this file somewhere - I copied it one to my local machine, then from there to the new Proxmox (same &lt;code&gt;/var/lib/vz/dump&lt;/code&gt; directory) since I was using FileZilla, but a hardcore scp user would have gone direct between the two servers and saved a bit of time.&lt;/p&gt;
&lt;p&gt;Now on the new server, I can see my backup! All you do then is select it and hit the &lt;code&gt;Restore&lt;/code&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-06-at-7.58.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A minute or two later, the VM &amp;ldquo;dockhost&amp;rdquo; is in the list. I press &lt;code&gt;Start&lt;/code&gt;, and it boots, my containers all start. And magically, amazingly it all works perfectly.&lt;/p&gt;
&lt;p&gt;If I wasn&amp;rsquo;t already sold on virtualization, this would definitely sell me on it. I understand there are other ways of moving VM&amp;rsquo;s between hosts, but this is hard to beat for simplicity if you can afford the downtime. This was the first time I&amp;rsquo;d ever done this, and I was stopping to screenshot things along the way. From the time I stopped the VM, to the time my last container went green was only nine minutes.&lt;/p&gt;</description></item><item><title>Uptime Kuma &amp; NTFY</title><link>https://blog.iankulin.com/uptime-kuma-ntfy/</link><pubDate>Wed, 15 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/uptime-kuma-ntfy/</guid><description>&lt;p&gt;&lt;a href="https://github.com/louislam/uptime-kuma"&gt;Uptime Kuma&lt;/a&gt; is a monitoring tool suitable for self-hosting, and as well as being a good tool for monitoring the status of your network and applications, it&amp;rsquo;s a nice smallish app to get started on Docker containers.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-6.41.24-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-6.41.24-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since it&amp;rsquo;s in a container, you need to create a volume for it and pass it in to persist your settings. Then it&amp;rsquo;s just a matter of adding each item you want to monitor. There&amp;rsquo;s a heap of fancy options for this, the only three I&amp;rsquo;ve used are ping - just pings an address, http(s) - requests a page and checks the header for a 200, and http(s) keyword - looks at the returned page for a keyword in the html.&lt;/p&gt;
&lt;p&gt;You choose the time intervals for all these. Additionally you can set up a notification for each. This is a great idea - I&amp;rsquo;m not sitting in my datacentre command room watching Uptime Kuma all day, I need to know on my phone if a CAT5 cable&amp;rsquo;s been pulled out inadvertently while I was vacuuming.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s lots of options for how to do this, including messaging platforms such as Telegram and Discord. I had a look on &lt;a href="https://www.reddit.com/r/selfhosted/comments/z0gpr2/free_push_service_for_uptime_kuma/"&gt;r/selfhosted&lt;/a&gt; to see what was recommended, and discovered &lt;a href="https://ntfy.sh/"&gt;NTFY&lt;/a&gt; which is an amazing little service. It has Android and iOS apps, in the app you subscribe to an endpoint, then a notification can be sent to your phone with a simple http get, for example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -d &amp;#34;This message will pop up on phone&amp;#34; ntfy.sh/ian_test
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-05-at-12.39.22-pm.png" alt=""&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4055.jpg" width="192" alt=""&gt;
&lt;p&gt;The &lt;code&gt;ian_test&lt;/code&gt; part of the url is called the &lt;em&gt;topic&lt;/em&gt;, and in the app you can subscribe to several topics. It&amp;rsquo;s worth noting this is all completely open. Anyone can send messages to the ian_test topic, and anyone can receive them. You should choose a topic name that&amp;rsquo;s likely to be unique, and be mindful that you&amp;rsquo;re leaking intelligence. For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -d &amp;#34;CCTV offline - 12 George St&amp;#34; ntfy.sh/maquarie_bank
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;would not a be a good use case. Get something more secure for that application. It&amp;rsquo;s probably not going to be free.&lt;/p&gt;
&lt;p&gt;Speaking of which, NTFY is free, including the server. It is possible (and probably a good idea since then you could add a little security) to self host it. It&amp;rsquo;s such a great little tool, and just so immediately and completely achieved what I wanted with zero drama and low effort, I hit the &lt;a href="https://github.com/sponsors/binwiederhier"&gt;github sponsor button for it&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>ssh key login on VPS</title><link>https://blog.iankulin.com/ssh-key-login-on-vps/</link><pubDate>Sun, 12 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-key-login-on-vps/</guid><description>&lt;p&gt;Due to &lt;a href="https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/"&gt;potential brute force attacks&lt;/a&gt;, it&amp;rsquo;s a good idea to turn off password access via shh and instead rely on ssh keys. In this post, I&amp;rsquo;ll run through that process.&lt;/p&gt;
&lt;h4 id="generating-your-key"&gt;Generating your key&lt;/h4&gt;
&lt;p&gt;On a mac (or actually most *ix systems), your ssh keys live in the &lt;code&gt;.ssh&lt;/code&gt; directory inside the users home directory. Since it starts with a period, it&amp;rsquo;s a &amp;lsquo;hidden&amp;rsquo; directory. To see it in Finder press&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Shift|Command|.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;(that command includes the full stop). Here&amp;rsquo;s mine:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-31-at-6.05.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The keys are in pairs, there are two pairs of keys above: id_ecdsa and id_rsa. Each pair of keys includes a public key and a private key. It&amp;rsquo;s the public key we want to put on the server. The private key is precious - it should not be anywhere that others can access it. The public key can safely be provided to a server that can use it to securely authenticate the holder of the private key by doing some complicated stuff.&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t already have a key pair, we need to create one. On Mac or Linux that is a &lt;a href="https://www.makeuseof.com/ssh-keygen-mac/"&gt;straightforward process in a terminal&lt;/a&gt;, on Windows I think the recommendation is usually to use &lt;a href="https://www.ssh.com/academy/ssh/putty/windows/puttygen"&gt;PuTTY&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="installing-your-key-on-the-target-system"&gt;Installing your key on the target system&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s possible to create a file on the system we want to ssh onto and to paste the public key into in, but from a Mac or Linux machine, we can use the &lt;code&gt;ssh-copy-id&lt;/code&gt; command which will do all that for us in an error-free way. It has the format:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh-copy-id &amp;lt;username&amp;gt;@&amp;lt;host&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-1.51.49-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-1.51.49-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="turning-off-password-access"&gt;Turning off password access&lt;/h4&gt;
&lt;p&gt;Although it&amp;rsquo;s convenient to ssh in without a password (because we&amp;rsquo;re using keys), the main reason for doing this is to turn off passwords to make brute-force password attacks impotent. For that, we need to turn off passwords for &lt;code&gt;ssh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note that I&amp;rsquo;m running on Unbuntu server 22.x so your mileage may vary. Certainly when I was reading around about this I found many slightly different approaches, but this is what I&amp;rsquo;ve done and tested so that ssh refuses to accept passwords.&lt;/p&gt;
&lt;p&gt;The configuration files for ssh on Ubuntu are at /etc/ssh/&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-3.57.15-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-3.57.15-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the &lt;code&gt;sshd_config&lt;/code&gt; we&amp;rsquo;re interested in (&lt;code&gt;ssh_config&lt;/code&gt; is for the client, we&amp;rsquo;re wanting to change the ssh server - daemon). You&amp;rsquo;ll also notice there&amp;rsquo;s a &lt;code&gt;sshd_config.d&lt;/code&gt; directory. The reason for this is that the config file has a line in it at the top that includes all the config files in that directory. This is a common pattern in Unbuntu - the main config file pulls in other config files. When you see that, you really shouldn&amp;rsquo;t edit the main config file as it&amp;rsquo;s possible that a future update will change it, you edit, or add to the files in the directory below.&lt;/p&gt;
&lt;p&gt;The way it words is that the commands in the files in the sub-directory will have priority over the defaults in the main config file (which is slightly counter-intuitive for me).&lt;/p&gt;
&lt;p&gt;The VPS I am using is on &lt;a href="https://blog.iankulin.com/your-own-aussie-server-on-binarylane/"&gt;binarylane&lt;/a&gt;, and they already have a config file in the subdirectory, so I&amp;rsquo;ll edit that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-4.08.24-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;With a standard Unbuntu install, there are no files in there, so you&amp;rsquo;ll need to create one with &lt;code&gt;touch&lt;/code&gt;, then add the line &lt;code&gt;PasswordAuthentication no&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Many of the articles on the internet also talk about turning off &lt;code&gt;ChallengeResponseAuthentication&lt;/code&gt; and &lt;code&gt;UsePAM&lt;/code&gt;, but I found with my VPS and local Unbuntu servers, all that was needed was &lt;code&gt;PasswordAuthentication&lt;/code&gt; if my intention was to prevent these attacks.&lt;/p&gt;
&lt;p&gt;Note that once this config is activated, you won&amp;rsquo;t be able to log in via ssh with a password, so it would be foolhardy to do it without having set up &lt;em&gt;and&lt;/em&gt; tested your login with keys.&lt;/p&gt;
&lt;p&gt;This config won&amp;rsquo;t be active until the ssh daemon is restarted.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now if someone attempts to ssh in they&amp;rsquo;ll see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-4.16.37-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Not that turning off passwords like this is only for ssh. You&amp;rsquo;ll still be able to log in via the console if you&amp;rsquo;ve stuffed something up.&lt;/p&gt;</description></item><item><title>Save Proxmox password in Chrome</title><link>https://blog.iankulin.com/save-proxmox-password-in-chrome/</link><pubDate>Sat, 11 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/save-proxmox-password-in-chrome/</guid><description>&lt;p&gt;When I installed Proxmox, I&amp;rsquo;d used a secure, and therefore absurdly long and complicated root password. I do use a password manager, but don&amp;rsquo;t have it integrated into Chrome, so it was buggging me having to find it and paste it in each time - why wasn&amp;rsquo;t Chrome offering to save it for me?&lt;/p&gt;
&lt;p&gt;Well, you&amp;rsquo;d guess it was something to do with this. I feel like Chrome is trying to tell me something here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.06.49-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Seems like a certificate thing. &lt;a href="https://forum.proxmox.com/threads/how-can-i-save-pve-web-loginpassword-on-firefox-chrome.46180/"&gt;These peeps&lt;/a&gt; say that I need to import the CA from PVE, and one more &lt;a href="https://pve.proxmox.com/wiki/Import_certificate_in_browser"&gt;googlestep reveals&lt;/a&gt; the certificate is on the Proxmox machine at &lt;code&gt;/etc/pve/pve-root-ca.pem&lt;/code&gt; so we need to grab that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/aint.jpg" width="90" alt=""&gt;
&lt;p&gt;A while ago, I wrote a post about &lt;a href="https://blog.iankulin.com/copying-a-file-via-ssh/"&gt;using scp to copy files over ssh&lt;/a&gt;, and you should totally know how to do that, but my daily drive for secure file copying is now &lt;a href="https://filezilla-project.org/"&gt;filezilla&lt;/a&gt;. Once you have a bundle of servers in VM&amp;rsquo;s and containers that you revisit and move stuff around all the time, its just a big productivity step-up to have that list of hosts and credentials a tap away, plus having the visual arrangement of nested folders works for my brain somehow.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.14.40-am-1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;On Mac, certificates need to live in the KeyChain, so you just drag the file into the certificates page. But it won&amp;rsquo;t be trusted, so you need to go in and manually do that. Where it says &amp;ldquo;Use System Defaults&amp;rdquo; change it to &amp;ldquo;Always Trust&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.19.54-am-1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It was annoying at this stage to find that Chrome was still saying it was insecure - even though it had changed to saying the certificate was valid.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.20.50-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Looking at the settings for the site in Chrome, there&amp;rsquo;s an option for &amp;ldquo;Insecure Content&amp;rdquo; I try changing that to &amp;ldquo;Allow&amp;rdquo;, but really I&amp;rsquo;m guessing by this stage.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.21.15-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;But it actually does help - I&amp;rsquo;ve got the little padlock. That wasn&amp;rsquo;t quite the end since Chrome still wasn&amp;rsquo;t offering to save the password, but clearing the cache fixed that.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.24.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-04-at-7.24.08-am.png" width="566" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Saved by the qemu_guest_agent</title><link>https://blog.iankulin.com/saved-by-the-qemu_guest_agent/</link><pubDate>Fri, 10 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/saved-by-the-qemu_guest_agent/</guid><description>&lt;p&gt;Literally an hour after I wrote the post &lt;a href="https://blog.iankulin.com/proxmox-qemu-guest-agent/"&gt;about installing the qemu guest agent&lt;/a&gt; in a VM and explaining how it can be used to inject root level commands into a VM, I had use of it due to a mistake.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d decided to add myself to the sudoers file. Since the last line in that file is a directive to include all the files in the /etc/sudoers.d directory, the accepted way to do that for local changes is to create a file in that directory with the necessary commands.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# User privilege specification
root	ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on &amp;#34;@include&amp;#34; directives:

@includedir /etc/sudoers.d
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The format of this command is important to get right, since if you stuff it up, sudo will not work, and I don&amp;rsquo;t even have a root login for this server, so then I&amp;rsquo;d be in a pickle. It&amp;rsquo;s so important to not stuff this up that there is a special command for editing the files that won&amp;rsquo;t let you save them if you&amp;rsquo;ve made a mistake.&lt;/p&gt;
&lt;p&gt;Out of an abundance of caution, I decided to copy the system sudoers file to the directory as a starting point since it would have the correct format and be easy to edit. It didn&amp;rsquo;t occur to me that then the &lt;code&gt;@includedir&lt;/code&gt; at the end would become an infinite loop.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-2.06.12-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So here I am, logged in as ian, with no sudo, needing to edit or delete a protected file, and with no root login. Luckily, it&amp;rsquo;s a VM running the qemu user agent, so I can access it from Proxmox.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-2.04.37-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-2.04.37-pm.png" width="895" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Saved by over-engineering! Thank you open source contributors.&lt;/p&gt;</description></item><item><title>Proxmox - Qemu-guest-agent</title><link>https://blog.iankulin.com/proxmox-qemu-guest-agent/</link><pubDate>Thu, 09 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-qemu-guest-agent/</guid><description>&lt;p&gt;One of the strengths of having virtual machines (VMs) running inside a hypervisor like Proxmox is how they are isolated from each other and their host. This is a strength - if there is a problem with a particular VM nothing else should be affected by it.&lt;/p&gt;
&lt;p&gt;But this can also be a pain if the hypervisor needs access to a VM to control or monitor it in some way that&amp;rsquo;s only possible from inside the VM. Proxmox can use the &lt;a href="https://qemu-project.gitlab.io/qemu/interop/qemu-ga.html"&gt;Qemu Guest Agent&lt;/a&gt; for this purpose. To over simplify, this is a deamon that runs in the VM and opens a unix socket/virtual serial port to the hypervisor, and listens for commands on it. With Proxmox, the main use of this is to aid in orderly shutdowns and backups, but it also allows us to run commands in the VM from Proxmox - an obvious security compromise. You definitely would not want to install this daemon on a hosted VPS.&lt;/p&gt;
&lt;h4 id="installing-qemu-guest-agent"&gt;Installing Qemu-guest-agent&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;m running Unbuntu Server 22.4.1 inside Proxmox 7.3 for the following examples.&lt;/p&gt;
&lt;p&gt;Use apt (or whatever you distro uses) to install the agent inside the VM.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;apt install qemu-guest-agent
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will do the usual thing - build the list, ask your permission to use the disk space, then download and unpack everything.&lt;/p&gt;
&lt;p&gt;Some guides on the internet will tell you to either use &lt;code&gt;systemctl&lt;/code&gt; to start the agent now, or to reboot the VM. Don&amp;rsquo;t do either of those.&lt;/p&gt;
&lt;p&gt;Instead, shutdown the VM entirely from Proxmox. Then in Proxmox, with the VM selected, we need to go into &lt;code&gt;Options&lt;/code&gt; and find &lt;code&gt;QEMU Guest Agent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.21.27-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;To change an option you either double click on the line your are interested in, or select it and click edit up the top. So do that for &lt;code&gt;QEMU Guest Agent&lt;/code&gt; and select the box to enable it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.33.05-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done. We&amp;rsquo;ll select the VM and start it. If you watch the summary screen as it starts, you&amp;rsquo;ll be able to see if everything is working by watching the IP Address field. It will start off saying &lt;em&gt;Guest Agent not running&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.37.33-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;But then change once the boot gets to the stage of running all the daemons. This is an example of the hypervisor being able to use the agent to get information about what&amp;rsquo;s going on inside the VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-9.33.53-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you want to double check everything is working, you can &lt;code&gt;ssh&lt;/code&gt; into the VM, and have a look at the process with &lt;code&gt;systemctl status qemu-guest-agent&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-12.07.46-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-12.07.46-pm.png" width="938" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Or, we can look from the host. If you select the shell of the node - remember mine was called &lt;code&gt;pve&lt;/code&gt;, you have a console for the root node that owns all the virtual machines. We can run qm with &lt;a href="https://qemu.readthedocs.io/en/latest/interop/qemu-ga-ref.html"&gt;all sorts of options&lt;/a&gt; to accomplish different things. One of the most interesting is &lt;code&gt;qm guest exec&lt;/code&gt; which allows us to run whatever we&amp;rsquo;d like, as root, on the guest vm.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-29-at-12.13.17-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The number 101 in &lt;code&gt;qm guest exec 101 -- hostname&lt;/code&gt; is the Proxmox id for the server we want to access - it&amp;rsquo;s shown in the server view in the top left, and the text after &lt;code&gt;--&lt;/code&gt; is the command to execute. What&amp;rsquo;s returned is some JSON with the exit code and the output. This should be a chilling reminder that anyone with access to the proxmox account will also have root access to all your VM&amp;rsquo;s running the daemon.&lt;/p&gt;</description></item><item><title>SSH &amp; the scary warning</title><link>https://blog.iankulin.com/ssh-the-scary-warning/</link><pubDate>Wed, 08 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-the-scary-warning/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-8.41.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The first time you connect to a new server with ssh, it asks you something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;➜ ~ &amp;gt; ssh ian@192.168.100.20 
The authenticity of host &amp;#39;192.168.100.20 (192.168.100.20)&amp;#39; can&amp;#39;t be established.
ED25519 key fingerprint is SHA256:ZcNTcOjO/0fOLC5iNChf8Q8MHN7z2d+VV0qz7XqH1g4.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added &amp;#39;192.168.100.20&amp;#39; (ED25519) to the list of known hosts.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you&amp;rsquo;ve said yes, it adds the server &amp;lsquo;fingerprint&amp;rsquo; to the known hosts file, then next time you ssh there, it feels safe - we know this server.&lt;/p&gt;
&lt;p&gt;But&amp;hellip;. if you&amp;rsquo;re playing around with virtual machines. Loading them, booting them, rebuilding them, cloning them etc. You might try and connect to a VM which is a different one from before, but which has the same ip address. SSH will not be happy:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;➜ ~ &amp;gt; ssh ian@192.168.100.20
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:ZcNTcOjO/0fOLC5iNChf8Q8MHN7z2d+VV0qz7XqH1g4.
Please contact your system administrator.
Add correct host key in /Users/user/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/user/.ssh/known_hosts:9
Host key for 192.168.100.20 has changed and you have requested strict checking.
Host key verification failed.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is right to be suspicious. From its point of view, it goes to Joe&amp;rsquo;s house each day, and it&amp;rsquo;s always Joe who answers the door. Today, it&amp;rsquo;s someone completely different but who says they are Joe. But since we know this is a different server, this is an expected result, so I&amp;rsquo;d like to ignore it.&lt;/p&gt;
&lt;p&gt;Although the message says to add the new fingerprint to the known_hosts file, it&amp;rsquo;s easier just to delete the old ones. Then when I try to connect to this server again, it will think it&amp;rsquo;s a new one and ask me to accept it. To delete this ip address (or hostname) out of the known hosts file, I just need to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;➜ ~ &amp;gt; ssh-keygen -R 192.168.100.20
# Host 192.168.100.20 found: line 7
# Host 192.168.100.20 found: line 8
# Host 192.168.100.20 found: line 9
/Users/user/.ssh/known_hosts updated.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And we&amp;rsquo;re good to go again. But thank you ssh for being so careful.&lt;/p&gt;</description></item><item><title>Proxmox - Installing a Virtual Machine</title><link>https://blog.iankulin.com/proxmox-installing-a-virtual-machine/</link><pubDate>Tue, 07 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/proxmox-installing-a-virtual-machine/</guid><description>&lt;p&gt;Installing your first virtual machine (VM) in the Proxmox hypervisor is pretty straightforward. This post runs through those steps using Proxmox 7.3.&lt;/p&gt;
&lt;p&gt;You need an operating system for your virtual machine, I&amp;rsquo;m going to use &lt;a href="https://ubuntu.com/download/server"&gt;Ubuntu server&lt;/a&gt; in this example, but it could just as easily be &lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2016-essentials"&gt;Windows server&lt;/a&gt;, or regular windows, or one of the desktop Linux distributions. Whichever you decide, you&amp;rsquo;ll need to find and download the ISO for it. The ISO is a (usually quite large) file needed to install the operating system.&lt;/p&gt;
&lt;p&gt;Once, you&amp;rsquo;ve got the ISO for the operating system, you need to upload it into Proxmox via the web interface. The ISO will be stored in the &lt;code&gt;local&lt;/code&gt; directory style storage. If you click on it in Proxmox, you&amp;rsquo;ll see there&amp;rsquo;s actually a section for ISOs, as well as buttons there to upload an ISO from your machine, or to directly download it into ProxMox from a link.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-27-at-5.45.54-pm-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Above you can seen I&amp;rsquo;ve now got two ISO images stored in my local storage. Once an image is there, you are ready to install it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.03.45-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In the top right of the Proxmox screen there are two blue buttons. One of them says &amp;ldquo;Create VM&amp;rdquo;, and that&amp;rsquo;s what we want to do. Now there will be a series of dialogs to click through and fill out. Most things we can just leave as defaults, but a few need some decisions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.06.56-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The node (your server) is already filled out. Mine is &lt;code&gt;pve&lt;/code&gt; since I just used the default name when I first installed Proxmox. The VM (virtual machine) ID is used by Proxmox to identify the server. You can change this to any three digit number you haven&amp;rsquo;t used. I&amp;rsquo;m keeping 100. Some people use this to separate their server types, for example all their production servers might be in the three hundreds.&lt;/p&gt;
&lt;p&gt;You need to come up with a name for this VM. These can only use letters and numbers - no punctuation. I like to keep them short, and describe the purpose of this VM, but perhaps you want to name yours after the OS you are using. I&amp;rsquo;m calling this one dockerhost because it&amp;rsquo;s going to host my Docker containers. Once you&amp;rsquo;ve decided, hit &lt;code&gt;next&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.15.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s where we choose the image, I&amp;rsquo;m going with the Unbuntu I downloaded earlier.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.16.46-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The System page - I&amp;rsquo;m just leaving all the defaults and hitting &lt;code&gt;next&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.18.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;On the Disks page, we go have a decision to make: how much drive space does this VM get. You&amp;rsquo;ll remember from our discussion about thin provisioning that we can allocate more disk than we have, but it&amp;rsquo;s not a good idea. The final decision about this is something you need to make considering the purpose of this VM and the space you&amp;rsquo;ve got available to you. You might need to google around for recommendations. It&amp;rsquo;s pretty easy to increase the disk size after your VM is created, but more difficult to reduce it.&lt;/p&gt;
&lt;p&gt;The Wizard has suggested 32GB for me, but the &lt;a href="https://linuxconfig.org/ubuntu-22-04-minimum-requirements"&gt;minimum spec is for 2.5GB&lt;/a&gt;. I am going to be downloading a few large containers, so 10GB seems like a good starting point for me.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.29.27-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Next is the CPU&amp;rsquo;s. Leave the defaults for everything, except you need to make a decision about the number of cores. My baby server only has two cores, but yours may have a eight or more. Proxmox will ration things out to some extent by time slicing - so you can easily run eight VM&amp;rsquo;s all allocated one core on a four core processor. And in fact, since a lot of them will probably just be sitting there waiting for something to happen, none of them will need to wait.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s probably a bad idea to allocate all of your cores to one VM, so I&amp;rsquo;m going to say &amp;lsquo;one&amp;rsquo; for mine, but you should also consider the processing needs of your VMs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.44.45-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Another important consideration is the amount of memory. Again, the needs will be determined by your use case. In my case, the minimum spec is for 1GB, but I&amp;rsquo;m planning on loading up some large containers and I have 8GB in hardware. So I&amp;rsquo;ll go with 4GB. The story with the minimum memory field is a &lt;a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_memory"&gt;little bit complicated&lt;/a&gt;, but basically, setting this lower than the max memory gives Proxmox a little bit of flexibility to share it around if you&amp;rsquo;re not using it all - which sounds like a good idea, so I&amp;rsquo;ll say my minimum is 2GB.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.47.21-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Networking in a visualized environment is a whole thing. But I have simple needs and only one hardware port, so all these defaults are fine for us.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.48.50-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The Confirm page is just a last chance to look over what we&amp;rsquo;ve chosen, then we can press &lt;code&gt;Finish&lt;/code&gt; to create our VM! A few seconds later it should be showing up in the server view. If we click on the VM in the server view, we can see the summary. It&amp;rsquo;s not very exciting yet because our machine is not running.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-3.57.42-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve highlighted the buttons we are going to use next in the image above. &lt;code&gt;Start&lt;/code&gt; is going to start the VM, and we&amp;rsquo;ll need to open the &lt;code&gt;Console&lt;/code&gt; to see what&amp;rsquo;s going on. Go ahead and click both of these now, and sit back in amazement.&lt;/p&gt;
&lt;p&gt;What happens next depends on what OS you are installing into this VM. You&amp;rsquo;ll just need to work your way through the questions accordingly. One point worth noticing though is that if is asks you questions like &amp;ldquo;Use the entire disk&amp;rdquo;, it&amp;rsquo;s talking about the virtual disk you allocated - not the physical disk.&lt;/p&gt;
&lt;p&gt;This operating system you&amp;rsquo;re installing now &lt;em&gt;doesn&amp;rsquo;t know&lt;/em&gt; it&amp;rsquo;s inside a virtual machine. Everything it sees - the machine bios, the screen, the memory - it&amp;rsquo;s all faked - and managed by Proxmox. You and Proxmox are playing god here. From the VM point of view, it could be installed directly on hardware. It doesn&amp;rsquo;t know the true nature of it&amp;rsquo;s world.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/pappademas_matrixkeanureeves.webp" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-4.15.24-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;While you are killing time waiting for your new OS to install, if you haven&amp;rsquo;t used noVNC before, it&amp;rsquo;s worth noticing the little slide in options on the left edge there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-6.45.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-6.45.34-pm.png" width="974" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I most commonly use it to force this window fullscreen, but in the &amp;ldquo;Extra Keys&amp;rdquo; button might be handy if you&amp;rsquo;re running a Windows OS and want the Windows key. I don&amp;rsquo;t love this console window - I&amp;rsquo;d rather SSH in and use my terminal, but it&amp;rsquo;s a handy tool that&amp;rsquo;s always going to work if the VM is running.&lt;/p&gt;</description></item><item><title>sudo Incident Reports - where do they go?</title><link>https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/</link><pubDate>Sat, 04 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/</guid><description>&lt;p&gt;Even though it&amp;rsquo;s &lt;em&gt;my&lt;/em&gt; server, I still have a pang of guilt when this happens.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-10.40.43-am-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I always imagine &lt;a href="https://en.wikipedia.org/wiki/Richard_Stallman"&gt;Richard Stallman&lt;/a&gt; (or someone with a similar 2000&amp;rsquo;s database administrator beard) looking at me disappointedly and shaking his head slowly.&lt;/p&gt;
&lt;p&gt;It does raise the question though - since it&amp;rsquo;s my server, shouldn&amp;rsquo;t I be getting a text message from CERN or something?&lt;/p&gt;
&lt;h4 id="where-is-this-report"&gt;Where is this report?&lt;/h4&gt;
&lt;p&gt;(&lt;a href="https://xkcd.com/838/"&gt;Relevant xkcd&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Like everything, the answer is &amp;lsquo;it&amp;rsquo;s logged&amp;rsquo;. We can use the &lt;code&gt;journalctl&lt;/code&gt; command to look at the logs, on this server that&amp;rsquo;s been running less than 20 hours, there&amp;rsquo;s already several thousand lines to look through if you just enter &lt;code&gt;journalctl&lt;/code&gt;, so I&amp;rsquo;m going to just send all the high priority logs to a file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;journalctl -p 3 &amp;gt; errors.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then since this just happened, it should be at the end of the file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tail errors.txt
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Jan 28 12:10:40 enrico-rider sshd[5168]: fatal: Timeout before authentication for 110.41.153.190 port 41826
Jan 28 12:11:01 enrico-rider sshd[5170]: fatal: Timeout before authentication for 110.41.153.190 port 41856
Jan 28 12:23:15 enrico-rider sshd[5222]: fatal: Timeout before authentication for 61.177.173.39 port 29421
Jan 28 12:23:26 enrico-rider sshd[5223]: fatal: Timeout before authentication for 61.177.173.39 port 49692
Jan 28 12:23:37 enrico-rider sshd[5226]: fatal: Timeout before authentication for 61.177.173.39 port 10416
Jan 28 12:39:51 enrico-rider sshd[5517]: fatal: Timeout before authentication for 61.177.172.108 port 53867
Jan 28 12:50:06 enrico-rider sshd[5653]: error: kex_exchange_identification: Connection closed by remote host
Jan 28 13:03:53 enrico-rider sshd[5696]: error: kex_exchange_identification: Connection closed by remote host
Jan 28 13:24:58 enrico-rider sshd[5804]: fatal: Timeout before authentication for 61.177.173.39 port 46041
Jan 28 13:40:06 enrico-rider sudo[6077]: ian : user NOT in sudoers ; TTY=pts/0 ; PWD=/home/ian ; USER=root ; COMMAND=/usr/bin/docker ps
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There we go, it really has been reported!&lt;/p&gt;
&lt;h4 id="how-to-add-a-user-to-the-sudoers-file"&gt;How to add a user to the sudoers file&lt;/h4&gt;
&lt;p&gt;To avoid these terrible reports, it sounds like I need to add myself to the &amp;lsquo;sudoers file&amp;rsquo;. I won&amp;rsquo;t be able to do that as myself, so I&amp;rsquo;ll log back in as &lt;code&gt;root&lt;/code&gt; for a bit. The reason I don&amp;rsquo;t just operate as &lt;code&gt;root&lt;/code&gt; all the time is that I quite like the constant reminder that I&amp;rsquo;m about to do something administratory - so I should have a second thought before I sudo that shell command I just copied out of a stackoverflow answer.&lt;/p&gt;
&lt;p&gt;Since the error message says I&amp;rsquo;m not in the sudoers file, I should just add my name right? Well yes, and no. That is possible, but it&amp;rsquo;s slightly dangerous - it has a specific format, and if you stuff things up bad things can happen. For this reason there&amp;rsquo;s a special command to edit it (visudo) which refuses to save it if you make a mistake.&lt;/p&gt;
&lt;p&gt;The sudoers file is at &lt;code&gt;/etc/sudoers&lt;/code&gt;, if you &lt;code&gt;cat&lt;/code&gt; it, it has a heap of commented out stuff, but there&amp;rsquo;s be a section that looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# User privilege specification
root	ALL=(ALL:ALL) ALL

# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo	ALL=(ALL:ALL) ALL

# See sudoers(5) for more information on &amp;#34;@include&amp;#34; directives:

@includedir /etc/sudoers.d
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That last line includes any files in the &lt;code&gt;/ect/sudoers.d&lt;/code&gt; as part of this one, so if we really did want to add &lt;code&gt;ian&lt;/code&gt; to this file, we&amp;rsquo;d do it there, but still by using the &lt;code&gt;visudo&lt;/code&gt; command to do it safely.&lt;/p&gt;
&lt;p&gt;But, we don&amp;rsquo;t need to. The &lt;code&gt;%admin&lt;/code&gt; and &lt;code&gt;%sudo&lt;/code&gt; lines are granting these permissions to groups, so all we need to do is add &lt;code&gt;ian&lt;/code&gt; to the &lt;code&gt;sudo&lt;/code&gt; group and those permissions will be granted, safely.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;usermod -a -G sudo ian
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Success:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ian@enrico-rider:~$ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get &amp;#34;http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json&amp;#34;: dial unix /var/run/docker.sock: connect: permission denied
ian@enrico-rider:~$ sudo docker ps
[sudo] password for ian: 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
520ed656ef12 dockersamples/101-tutorial &amp;#34;nginx -g &amp;#39;daemon of…&amp;#34; 14 hours ago Up 14 hours 0.0.0.0:80-&amp;gt;80/tcp, :::80-&amp;gt;80/tcp pedantic_bartik
ian@enrico-rider:~$ 
&lt;/code&gt;&lt;/pre&gt;</description></item></channel></rss>