<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Docker on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/docker/</link><description>Recent content in Docker on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Sat, 10 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/docker/index.xml" rel="self" type="application/rss+xml"/><item><title>VS Code Dev Containers</title><link>https://blog.iankulin.com/vs-code-dev-containers/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/vs-code-dev-containers/</guid><description>&lt;h3 id="remote-ssh"&gt;Remote-SSH&lt;/h3&gt;
&lt;p&gt;One of the things I&amp;rsquo;ve done a bit in Visual Studio Code is using it&amp;rsquo;s ability to work on a different machine over SSH. I have a couple of LXCs on a server set up for different languages - one for C++ and another for Rust. They are things I don&amp;rsquo;t work in often, and I didn&amp;rsquo;t want to set them up on my laptop, but thought I might want them again sometime in the future.&lt;/p&gt;
&lt;p&gt;This is straightforward in VS Code - You install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;remote ssh extension&lt;/a&gt; locally, then choose &lt;code&gt;Remote-SSH: Connect to Host&lt;/code&gt; in the command palette. A few seconds later, it appears that you&amp;rsquo;re working locally, but actually you are working in a remote session on the server your VS Code instance is SSH&amp;rsquo;d into (a small indicator in the bottom left is the tell). You need to clone your project from git since you&amp;rsquo;re working in the server&amp;rsquo;s file system, and there&amp;rsquo;s some complexity with extensions since you&amp;rsquo;re sort of working in a new VS Code instance, but apart from that it feels the same as working locally.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/architecture-ssh.png" width="968" alt="Remote SSH VS Code architecture from https://code.visualstudio.com/docs/remote/ssh"&gt;
&lt;p&gt;The official docs for &lt;a href="https://code.visualstudio.com/docs/remote/ssh"&gt;Remote Development using SSH are here.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="why-now"&gt;Why now?&lt;/h3&gt;
&lt;p&gt;The reason I&amp;rsquo;ve been interested in this again lately is to provide a more secure environment for using AI agentic coding tools like Claude Code. If it&amp;rsquo;s running in it&amp;rsquo;s own server (that I can easily recreate from a snapshot) I don&amp;rsquo;t have to worry about dramas like having &lt;a href="https://www.theregister.com/2025/12/01/google_antigravity_wipes_d_drive/"&gt;my hard drive deleted&lt;/a&gt; by Gemini.&lt;/p&gt;
&lt;p&gt;But what if you don&amp;rsquo;t have the ability to spin up an environment on your homelab? There&amp;rsquo;s a few options, but probably the easiest for VS Code users is to work in a &amp;lsquo;Dev Container&amp;rsquo;.&lt;/p&gt;
&lt;h3 id="dev-containers"&gt;Dev Containers&lt;/h3&gt;
&lt;p&gt;Working in a Dev Container is basically the same as remoting into another server with VS Code, but in this case the other server is a container spun up in Docker.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/architecture-containers.png" width="968" alt="VS Code Dev Container architecture. From https://code.visualstudio.com/docs/devcontainers/containers"&gt;
&lt;p&gt;There&amp;rsquo;s a couple of important differences from working on a remote server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we&amp;rsquo;re working on our local files;&lt;/li&gt;
&lt;li&gt;Dev containers are an important system for sharing repeatable development environments, so it&amp;rsquo;s well integrated into VS Code - the tooling is nice.&lt;/li&gt;
&lt;li&gt;You need Docker/Docker desktop running locally.&lt;/li&gt;
&lt;li&gt;You need a container image to work with, and a &lt;code&gt;devcontainer.json&lt;/code&gt; file to tell VS Code how to manage all this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this post will focus on the basics of working in a Dev Container. There is also a good explanation of all this in the &lt;a href="https://code.visualstudio.com/docs/devcontainers/tutorial"&gt;VS Code docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-container"&gt;The Container&lt;/h2&gt;
&lt;p&gt;After you&amp;rsquo;ve installed the VS Code &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"&gt;extension&lt;/a&gt;, and set up your local Docker environment, you&amp;rsquo;re going to need a Docker container to work in. There&amp;rsquo;s a &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"&gt;big list of existing containers&lt;/a&gt; that cover most scenarios, but I prefer to roll my own. This is achieved by writing a Dockerfile.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM node:24-bookworm
&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;# Use non-root user for development
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;USER node
&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;# Development environment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENV NODE_ENV=development
&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;# Default command
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;npm&amp;#34;, &amp;#34;start&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m going to name this &lt;code&gt;Dockerfile.dev&lt;/code&gt; and put it in the &lt;code&gt;.devcontainer&lt;/code&gt; directory - later we&amp;rsquo;ll point to it from our &lt;code&gt;devcontainer.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-16.57.06.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The Dockerfile describes the system that we&amp;rsquo;ll be working in when we&amp;rsquo;re working on our project - it&amp;rsquo;s like the specification of our remote &amp;lsquo;server&amp;rsquo;. Sometimes you might want to install other stuff here - for example if you are a vim enthusiast, you might &lt;code&gt;apt install vim&lt;/code&gt; in the Dockerfile, but generally you should keep it reasonably generic.&lt;/p&gt;
&lt;h3 id="devcontainerjson"&gt;devcontainer.json&lt;/h3&gt;
&lt;p&gt;Next we need to tell VS Code how to work in the container we&amp;rsquo;ve specified.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Node.js Dev Container&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;build&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dockerfile&amp;#34;: &amp;#34;Dockerfile.dev&amp;#34;
&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; &amp;#34;forwardPorts&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 3000
&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; &amp;#34;postCreateCommand&amp;#34;: &amp;#34;npm install&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This goes in the .devcontainer directory. It&amp;rsquo;s almost all self explanatory - we tell the extension what container we&amp;rsquo;re using - in this case building it from &lt;code&gt;Dockerfile.dev&lt;/code&gt;, export the port we&amp;rsquo;re running on, and run a command in the terminal of our new system once it exists.&lt;/p&gt;
&lt;h3 id="whats-missing"&gt;What&amp;rsquo;s missing?&lt;/h3&gt;
&lt;p&gt;Alert readers will have noticed there&amp;rsquo;s a couple of things that we need which are missing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;VS Code Server - the way that VS Code is working in this configuration is that it&amp;rsquo;s running on our local machine, but connecting to a &amp;ldquo;VS Code Server&amp;rdquo; inside the container.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bind mounts to our local directory - Once we load up the dev container, all our local files will need to be available in VS Code somehow.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So how to these get in the container? We don&amp;rsquo;t have to do anything to deal with these two issues. This is part of the magic that the VS Code Dev Container extension is doing for us. After the container is created, the extension:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;installs the server binary, and starts it&lt;/li&gt;
&lt;li&gt;bind mounts the local workspace to &lt;code&gt;/workspaces/&amp;lt;your-folder-name&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And sets that as the WORKDIR in the container&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="lets-go"&gt;Let&amp;rsquo;s go&lt;/h3&gt;
&lt;p&gt;Okay, with the &lt;code&gt;Dockerfile.dev&lt;/code&gt; and &lt;code&gt;devcontainer.json&lt;/code&gt; in place, we&amp;rsquo;re now ready to launch our devcontainer.&lt;/p&gt;
&lt;p&gt;In the VSCode command pallet, run &lt;code&gt;Dev Containers: Reopen in container&lt;/code&gt;. The first run will take a few moments since the container has to be built first, but then it should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.49.40.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The big clue that you&amp;rsquo;re in the dev container now is the blue message in the bottom left corner. Most everything that we could do in the local environment we can do here in the container - for example editing code and running it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.54.43.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.54.53.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="extensions"&gt;Extensions&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-18.05.45.png" width="427" alt=""&gt;
&lt;p&gt;If you have a look at your VS Code extensions while working in this dev container, you&amp;rsquo;ll see that some are still installed, but others are now greyed out. Some extensions only effect the UI (&amp;ldquo;UI Extensions&amp;rdquo; - listed under &amp;ldquo;Local - Installed&amp;rdquo;) so they live in the local VS Code, others have functionality that needs to run in the workspace environment so they (&amp;ldquo;Workspace extensions&amp;rdquo;) need to live in the VS Code server part.&lt;/p&gt;
&lt;p&gt;Any UI Extensions you had installed before will still be there since they live in the local VS Code, but they others will be greyed out and unavailable unless you install them into the dev container.&lt;/p&gt;
&lt;p&gt;In the image here you can see my &amp;ldquo;vscode-icons&amp;rdquo; and &amp;ldquo;vscode-pdf&amp;rdquo; which only effect how things are displayed are both still available, but &amp;ldquo;Beancount&amp;rdquo; &amp;amp; &amp;ldquo;Cline&amp;rdquo; that need access to files need to be installed in the container if I want to use them on this project.&lt;/p&gt;
&lt;p&gt;We can click on the &amp;ldquo;Install in Dev Container&amp;rdquo; button for any of these, and it will be installed into the dev container. I need ESLint for this project so I&amp;rsquo;ll could that - then it will appear in the &amp;lsquo;Dev Container&amp;rsquo; tab. This is a good way of mimicking the normal workflow for users, but this extension is only persisted in the container while it exists - If we make any changes to the &lt;code&gt;dockerfile&lt;/code&gt; or &lt;code&gt;devcontainer.json&lt;/code&gt; VS Code will rebuild the container and the extension will be gone again.&lt;/p&gt;
&lt;p&gt;If we want a workspace extension, a more persistent way to install it is to add it to our &lt;code&gt;devcontainer.json&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="adding-extensions-to-devcontainerjson"&gt;Adding extensions to devcontainer.json&lt;/h3&gt;
&lt;p&gt;Extensions added this way will be the same for anyone who uses our dev container definition (which is what I&amp;rsquo;m calling the combined &lt;code&gt;dockerfile&lt;/code&gt; and &lt;code&gt;devcontainer.json&lt;/code&gt;), including if they just clone the project from git. In fact, this was the original intention of dev containers - to have a fully reproducible development environment that is stored with its project.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s add a couple of extensions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Node.js Dev Container&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;build&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dockerfile&amp;#34;: &amp;#34;Dockerfile.dev&amp;#34;
&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; &amp;#34;customizations&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;vscode&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;extensions&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dbaeumer.vscode-eslint&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;esbenp.prettier-vscode&amp;#34;
&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&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; &amp;#34;forwardPorts&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 3000
&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; &amp;#34;postCreateCommand&amp;#34;: &amp;#34;npm install&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you make these changes in VS Code, it will probably pop up and ask you if it can rebuild the container, otherwise open the command palette and select &amp;ldquo;Dev Containers: Rebuild Container&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You might be wondering where we get the extension id&amp;rsquo;s from (like &lt;code&gt;dbaeumer.vscode-eslint&lt;/code&gt;) The easiest way to to right click on one in the extension list and select &amp;ldquo;Copy Extension ID&amp;rdquo;. It&amp;rsquo;s also listed on the extension web page.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-10-at-15.23.47.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="finally"&gt;Finally&lt;/h3&gt;
&lt;p&gt;So that&amp;rsquo;s the basics of getting a simple dev container set up. The code for this is on &lt;a href="https://github.com/IanKulin/devcont-demo"&gt;GitHub here&lt;/a&gt;. There&amp;rsquo;s a crucial issue we haven&amp;rsquo;t solved though - how to pull in your ssh keys for pushing the code to external repositories. We&amp;rsquo;ll deal with that in a future post.&lt;/p&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker pull --platform linux/amd64 jellyfin/jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once it&amp;rsquo;s pulled down, we output it to a file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker save -o jellyfin.image jellyfin/jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker save -o jellyfin.tar jellyfin/jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker &lt;span style="color:#81a1c1"&gt;load&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;i jellyfin&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;image
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>Perils of Benchmarking</title><link>https://blog.iankulin.com/perils-of-benchmarking/</link><pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/perils-of-benchmarking/</guid><description>&lt;p&gt;I&amp;rsquo;ve been containerising my websites, with their servers to make deployment simple and robust, and to move to a CI/CD workflow. Since an install of a production web server is large, I would be running about ten of these containers, and there&amp;rsquo;s already a good server facing the net and doing the reverse-proxying (NGINX Proxy Manager), I chose to bundle the Busy-Box httpd server with my sites inside the Docker containers.&lt;/p&gt;
&lt;p&gt;I had a vague feeling that there was a performance vs size compromise involved, and during some googling found this &lt;a href="https://github.com/nerkn/nginx-busybox-apache/tree/main"&gt;github repo&lt;/a&gt; where nerkn has bench-marked busy-box vs apache vs nginx with, to me (because of my choice above), alarming results.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-10.37.19-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If NGINX is doing twice the throughput, and is two orders of magnitude quicker, then busy-box is not going to be a good choice for me.&lt;/p&gt;
&lt;p&gt;Before I panicked, I thought I&amp;rsquo;d do my own A/B tests, which since it&amp;rsquo;s containerised is simple. I used the &lt;a href="https://httpd.apache.org/docs/2.4/programs/ab.html"&gt;apache &lt;code&gt;ab&lt;/code&gt; testing tool&lt;/a&gt; - it spits out the basics - times for connecting, processing, and waiting. It does multiple tests and gives you the mean and standard deviation for them. Perfect.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the results for a series of tests. I included a commercial website I suspect is in the same data centre as a sanity check.&lt;/p&gt;
&lt;table class="has-fixed-layout"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Test&lt;/td&gt;&lt;td&gt;Mean time (ms)&lt;/td&gt;&lt;td&gt;St dev&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nextdc.com.au&lt;/td&gt;&lt;td&gt;834&lt;/td&gt;&lt;td&gt;293&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;450&lt;/td&gt;&lt;td&gt;93&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;411&lt;/td&gt;&lt;td&gt;24&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;423&lt;/td&gt;&lt;td&gt;24&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;410&lt;/td&gt;&lt;td&gt;26&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;398&lt;/td&gt;&lt;td&gt;19&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;busy-box uclibc&lt;/td&gt;&lt;td&gt;419&lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;403&lt;/td&gt;&lt;td&gt;16&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nginx-alpine&lt;/td&gt;&lt;td&gt;398&lt;/td&gt;&lt;td&gt;23&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;nextdc.com.au&lt;/td&gt;&lt;td&gt;759&lt;/td&gt;&lt;td&gt;306&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Huh. A couple of things jump out. One is that the site is probably fast enough, and the other is that the performance of busy-box and NGINX are similar, like very suspiciously similar. I wonder what happens if I &lt;code&gt;docker compose down&lt;/code&gt; the website and run the test again?&amp;hellip;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;This is ApacheBench, Version 2.3 &amp;lt;$Revision: 1903618 $&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Licensed to The Apache Software Foundation, http://www.apache.org/
&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;Concurrency Level: 10
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Time taken for tests: 4.608 seconds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Complete requests: 100
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Failed requests: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Non-2xx responses: 100
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Total transferred: 30300 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HTML transferred: 15400 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Requests per second: 21.70 [#/sec] (mean)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Time per request: 460.777 [ms] (mean)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Time per request: 46.078 [ms] (mean, across all concurrent requests)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Transfer rate: 6.42 [Kbytes/sec] received
&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;Connection Times (ms)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; min mean[+/-sd] median max
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Connect: 274 318 33.6 306 451
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Processing: 82 95 14.5 92 170
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Waiting: 82 95 14.3 92 169
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Total: 362 413 37.5 401 580
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;lol. Okay. I guess I&amp;rsquo;ve been testing the cache in NGINX Proxy Manager this whole time. There is a setting for that, so perhaps I should turn that off. Sadly, even with that turned off, and the container not running, I&amp;rsquo;m still getting that good performance which would be the 500 error coming back from NGINX Proxy Manager.&lt;/p&gt;
&lt;p&gt;Time to trick it into not using the cache by making unique requests each time. I&amp;rsquo;ll use these:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ab -n 100 -c 10 &amp;#34;https://example.com.au/index.html?nocache=$(date +%s%N)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ab -n 100 -c 10 &amp;#34;https://www.nextdc.com/index.html?nocache=$(date%20+%s%N)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m also able to see that it&amp;rsquo;s hitting the container with all the requests by running the compose up in the foreground and having the logs output. So now I&amp;rsquo;m much more confident about the output. Here&amp;rsquo;s the summary of a much larger group of tests run in that round robin style.&lt;/p&gt;
&lt;table class="has-fixed-layout"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Situation&lt;/td&gt;&lt;td&gt;Mean time (ms)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Nextdc.com&lt;/td&gt;&lt;td&gt;617&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NGINX Proxy with no site&lt;/td&gt;&lt;td&gt;412&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NGINX-apline site&lt;/td&gt;&lt;td&gt;420&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Busy-box (uclibc)&lt;/td&gt;&lt;td&gt;424&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The comparison with nextdc is of course unfair. They are returning a lot more html, and some of it could be server rendered. I don&amp;rsquo;t have an explanation of why my results are so different from nerkn&amp;rsquo;s. He&amp;rsquo;s using a different tool, and I imagine on a local network (mine is over a mobile data link, to a VPS in a data centre).&lt;/p&gt;
&lt;p&gt;As far as the container size comparisons go, the NGINX-alpine one is 48.98MB and the uclibc version of BusyBox is 1.35MB. I think I&amp;rsquo;ll be sticking with that.&lt;/p&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; influxdb&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; influxdb&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; influxdb
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; healthcheck&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; test&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;CMD-SHELL&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;curl -f http://localhost:8086/ping&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; interval&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;5&lt;/span&gt;s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timeout&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; retries&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;8086:8086&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; DOCKER_INFLUXDB_INIT_MODE&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;setup
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; DOCKER_INFLUXDB_INIT_USERNAME&lt;span style="color:#81a1c1"&gt;=$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;INFLUXDB_ADMIN_USER&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; DOCKER_INFLUXDB_INIT_PASSWORD&lt;span style="color:#81a1c1"&gt;=$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;INFLUXDB_ADMIN_PASSWORD&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; DOCKER_INFLUXDB_INIT_ORG&lt;span style="color:#81a1c1"&gt;=$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;INFLUXDB_ORG&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; DOCKER_INFLUXDB_INIT_BUCKET&lt;span style="color:#81a1c1"&gt;=$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;INFLUXDB_BUCKET&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; DOCKER_INFLUXDB_INIT_ADMIN_TOKEN&lt;span style="color:#81a1c1"&gt;=$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;INFLUXDB_ADMIN_TOKEN&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; INFLUXD_METRICS_DISABLED&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;influxdb&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;lib&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;influxdb2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; unless&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;stopped
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; glimpse-scan:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: ghcr.io/iankulin/glimpse_scan:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: glimpse-scan
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; depends_on:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; influxdb:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; condition: service_healthy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; build:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context: .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dockerfile: Dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./glimpse-scan/data:/app/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; env_file:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - .env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM busybox&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Add shell script and set executable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY update_content&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;usr&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;local&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;bin&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;update_content&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN chmod &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt;x &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;usr&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;local&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;bin&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;update_content&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Create the directory for the web content, and copy files in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mkdir &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;p &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY www&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Expose port 80 for the web server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Start the httpd server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sh&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/usr/local/bin/update_content.sh &amp;amp; busybox httpd -f -p 80 -h /var/www/html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the bash script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Define the URL and the destination path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;URL&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;http://httpbin.org/image/jpeg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DEST_PATH&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/var/www/html/image.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FETCH_INTERVAL&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#b48ead"&gt;120&lt;/span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# 2 minutes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;while&lt;/span&gt; true&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# Use wget to download the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wget -O &lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;$DEST_PATH&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;$URL&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# Check the exit status of wget&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;[&lt;/span&gt; $? -eq &lt;span style="color:#b48ead"&gt;0&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;]&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;File downloaded successfully to &lt;/span&gt;$DEST_PATH&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Failed to download the file.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sleep $FETCH_INTERVAL
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; example.com:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: httpd-example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /etc/ssl/certs:/etc/ssl/certs:ro # Bind mount host&amp;#39;s SSL certs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name httpd-example.com -p 80:80 -v /etc/ssl/certs:/etc/ssl/certs:ro ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Define the URL and the destination path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;URL&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;http://httpbin.org/image/jpeg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DEST_PATH&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/var/www/html/image.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FETCH_INTERVAL&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#b48ead"&gt;120&lt;/span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# 2 minutes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;while&lt;/span&gt; true&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# Use wget to download the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wget -O &lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;$DEST_PATH&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;$URL&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# Check the exit status of wget&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;[&lt;/span&gt; $? -eq &lt;span style="color:#b48ead"&gt;0&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;]&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;File downloaded successfully to &lt;/span&gt;$DEST_PATH&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Failed to download the file.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sleep $FETCH_INTERVAL
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM busybox&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Add shell script and set executable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY update_content&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;usr&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;local&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;bin&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;update_content&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN chmod &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt;x &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;usr&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;local&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;bin&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;update_content&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Create the directory for the web content, and copy files in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mkdir &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;p &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY www&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Expose port 80 for the web server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Start the httpd server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sh&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/usr/local/bin/update_content.sh &amp;amp; busybox httpd -f -p 80 -h /var/www/html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; example.com:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: httpd-example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - nginx-proxy-manager_default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager_default:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; external: true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM busybox&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Create the directory for the web content, and copy files in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mkdir &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;p &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY www&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Expose port 80 for the web server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Start the httpd server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sh&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;busybox httpd -f -p 80 -h /var/www/html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To use this dockerfile to build our container, just docker build it and give it a tag:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build -t ghcr.io/iankulin/example.com:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then if we run it, and go to http://localhost, there&amp;rsquo;s our website.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name httpd-example.com -p 80:80 ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.15.11-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="with-nginx-proxy-manager"&gt;With NGINX Proxy Manager&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; file I use on the VPS host is slightly more complicated. We want each of the website containers to run in the same docker network as Nginx Proxy Manager - since docker networks have their own little dns server based on the container names, that&amp;rsquo;s going to make hooking it up trivial.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; example.com:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: httpd-example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - nginx-proxy-manager_default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager_default:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; external: true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="architecture"&gt;Architecture&lt;/h3&gt;
&lt;p&gt;Since I develop on an M1 MacBook, but host all my workloads on regular AMD64 Linux LXC containers or VMs, I need to build for that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build --platform linux/amd64 -t ghcr.io/iankulin/example.com:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In actual fact, I could have built that way for the Mac as well - Docker Desktop would have just run it in a Linux VM with a small performance penalty which wouldn&amp;rsquo;t be noticeable for my purposes. Once it&amp;rsquo;s built, we push it up to the GitHub Container Registry.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push ghcr.io/iankulin/example.com:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Working with the registry is well covered in my previous post, so I won&amp;rsquo;t go into those details here.&lt;/p&gt;
&lt;h3 id="on-the-host"&gt;On the host&lt;/h3&gt;
&lt;p&gt;On the host where the website is to run, I just make a directory for it and drop the &lt;code&gt;docker-compose.yml&lt;/code&gt; in. Then &lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-7.46.31-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running in the Nginx Proxy Manager docker network, when we specify the host name for the new web site for NPM to proxy to, it&amp;rsquo;s just the container name we gave it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.06.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then the DNS settings for your domain need to be pointed to this host. Once that&amp;rsquo;s propagated, you&amp;rsquo;ll be able to request the SSL certificate in NPM and your website is live.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-10-19-at-8.11.23-pm.png" width="886" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker login --username &amp;lt;github username&amp;gt; --password &amp;lt;PAT we just generated&amp;gt; ghcr.io
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push ghcr.io/&amp;lt;github user name&amp;gt;/&amp;lt;container name&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker pull ghcr.io/&amp;lt;github user name&amp;gt;/&amp;lt;container name&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-10-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>Containerised NGINX Proxy Manager &amp; the 502 error</title><link>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</link><pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/containerised-nginx-proxy-manager-the-502-error/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-6.46.49-am.png" width="695" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re used to running NGINX Proxy Manager in front of your web apps, and switch to running it in a container, you&amp;rsquo;re going to need to learn a little about Docker networks to get everything connected. If you just do your regular setup, and direct the proxy for an address to &lt;code&gt;127.0.0.1:&amp;lt;some port&amp;gt;&lt;/code&gt;, it won&amp;rsquo;t exist, and you&amp;rsquo;ll visit your page to find the &amp;ldquo;502 Bad Gateway openresty&amp;rdquo; message.&lt;/p&gt;
&lt;p&gt;If you pause to think for a second it will be obvious why - with NGINX Proxy Manager (I&amp;rsquo;m going to start calling it NPM to save myself some typing) &lt;em&gt;inside&lt;/em&gt; a container, any addresses you&amp;rsquo;re entering into the web interface when setting up proxys are &lt;em&gt;inside&lt;/em&gt; the NPM container. &lt;code&gt;127.0.0.1&lt;/code&gt; from that point of view refers to the inside of the NPM container, and not the host, so you exposing a port from your container to the host is not going to work.&lt;/p&gt;
&lt;p&gt;The fix for this is pretty simple, but first let&amp;rsquo;s look at the exception.&lt;/p&gt;
&lt;h3 id="the-exception"&gt;The Exception&lt;/h3&gt;
&lt;p&gt;Usually the very first proxy I add in NPM is to it&amp;rsquo;s own admin interface on port 81. Since this &lt;em&gt;is&lt;/em&gt; inside the NPM container, the setup looks exactly the same as if you were running on the host, and may well be why you were lulled into a false sense of familiarity.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.14.55-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a little bit confusing, since we can also manually access NPM from &lt;code&gt;http://127.0.0.1:81&lt;/code&gt; on the host if we&amp;rsquo;ve exposed port 81 in our compose file, but it&amp;rsquo;s actually a different route. In fact, we could hide port 81 from the host, and the setting above will still work.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my compose file, notice I haven&amp;rsquo;t exposed port 81&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: &amp;#39;jc21/nginx-proxy-manager:latest&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: nginx-proxy-manager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#39;80:80&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#39;443:443&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./letsencrypt:/etc/letsencrypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So if we try to access port 81 from the host, it won&amp;rsquo;t be answering.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.25.39-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But inside the container, 127.0.0.1 refers to itself, so if we open a shell into the container, the URL http://127.0.0.1:81 refers to something that exists, from there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.30.49-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="getting-out"&gt;Getting Out&lt;/h3&gt;
&lt;p&gt;So how can our NPM point to a service that&amp;rsquo;s running in another container? You are probably used to specifying ports: in your compose file to expose internal container ports to a host, but that doesn&amp;rsquo;t really help us since NPM in a container can not easily access the host&amp;rsquo;s ports. What we&amp;rsquo;d really like is for NPM to be able to access the second container&amp;rsquo;s ports. And in an ideal world, we&amp;rsquo;d like that to be the only way to access them - that way all the access to our second container service is forced through our proxy. Sounds like security no?&lt;/p&gt;
&lt;p&gt;Turns out this is simple thanks to the magic of Docker networks.&lt;/p&gt;
&lt;p&gt;You can get a fair way with Docker without really thinking or knowing about &lt;a href="https://docs.docker.com/engine/network/"&gt;Docker networks&lt;/a&gt;, and I&amp;rsquo;m really only covering the very basics here - you should probably invest some time in learning about this sometime. Meanwhile, lets run the &lt;code&gt;docker network ls&lt;/code&gt; command and see what networks we&amp;rsquo;ve got.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.41.33-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That network &lt;code&gt;nginx-proxy-manager_default&lt;/code&gt; is the one we&amp;rsquo;re interested in. Its name is just the container name with &amp;ldquo;_default&amp;rdquo; added on the end. What we need to do is just make sure the second container is included in that same network. That&amp;rsquo;s a matter of declaring the external network with that name, and including it in our service definition. I&amp;rsquo;m going to use an NGINX server in my example, but it could be anything. Here&amp;rsquo;s the compose for the second container.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-example.com:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: nginx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: nginx-example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./www:/usr/share/nginx/html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./conf/:/etc/nginx/conf.d/:ro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - nginx-proxy-manager_default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nginx-proxy-manager_default:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; external: true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that I haven&amp;rsquo;t exposed any ports to the host here; We&amp;rsquo;re not going to need them since we&amp;rsquo;ll access this container direct from the NPM container via the internal Docker network docker created for us. That little network even contains a DNS server, so we don&amp;rsquo;t even need to worry about the container&amp;rsquo;s IP addresses, we can just use their names.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-24-at-9.52.54-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So any web requests to &amp;ldquo;example.com&amp;rdquo; arriving at our host go to NPM (since I&amp;rsquo;ve exposed ports 80 &amp;amp; 443 - see the top compose file). Then using the proxy I&amp;rsquo;ve added above, they are forwarded to the container named &amp;ldquo;nginx-example.com&amp;rdquo; which is a DNS record inside the Docker network that Docker created for us, and which both the NPM, and my service, containers are members of.&lt;/p&gt;</description></item><item><title>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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uptime-kuma:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: louislam/uptime-kuma:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: uptime-kuma
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - kuma_data:/app/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - 80:3001
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kuma_data:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; jellyfin:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: jellyfin/jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; network_mode: host
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#34;8096:8096&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/config:/config
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/cache:/cache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /mnt/media:/media
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;a &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;lib&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;docker&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;volumes&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;uptimekuma_kuma_data&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;_data&lt;span style="color:#81a1c1"&gt;/.&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;~/&lt;/span&gt;uptimekuma&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@ct390-test:~/uptimekuma$ sudo docker volume ls
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DRIVER VOLUME NAME
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;local uptimekuma_kuma_data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@ct390-test:~/uptimekuma$ sudo docker volume rm uptimekuma_kuma_data 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uptimekuma_kuma_data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uptime-kuma:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: louislam/uptime-kuma:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: uptime-kuma
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data:/app/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - 80:3001
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>LLM coding question comparison using Ollama</title><link>https://blog.iankulin.com/llm-coding-question-comparison-using-ollama/</link><pubDate>Mon, 29 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/llm-coding-question-comparison-using-ollama/</guid><description>&lt;p&gt;Now Ollama has made it simple enough for anyone who can use a terminal to run large language models locally, naturally I&amp;rsquo;ve gone overboard downloading too many to play with. I&amp;rsquo;m increasingly feeling they definitely have a place in the devops/coding arsenal of tools, but which model is best?&lt;/p&gt;
&lt;p&gt;If you go on HuggingFace to look at a new model you&amp;rsquo;re interested, they often have great comparisons like this.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"&gt;&lt;img src="https://blog.iankulin.com/images/performance.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There has been a lot of work in crafting these and other benchmarks which are often comprehensive and well thought out. I&amp;rsquo;ve also seen people doing fun things, like &lt;a href="https://youtu.be/B0uMFWAGUzI?t=145"&gt;this guy&lt;/a&gt;, who is just pasting coding challenges off a web page into an LLM and seeing if it can solve them (spoiler - mostly it can solve &lt;a href="https://www.w3resource.com/python-exercises/basic/python-basic-1-exercise-141.php"&gt;coding problems that are probably part of it&amp;rsquo;s training set&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;A factor to keep in mind when looking at these charts is that they are probably running unquantised (uncompressed is a close enough analogy) models on fleets of &lt;a href="https://www.nvidia.com/en-au/data-center/h100/"&gt;$60K graphics cards&lt;/a&gt;. I can use that if I pay them $20 a month and have an internet connection, but I want to pay $0 and run it on my M1 MacBook - that&amp;rsquo;s why I downloaded &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So what follows is my completely unscientific testing of the models I&amp;rsquo;ve downloaded. Basically, I&amp;rsquo;ll ask them the same question (that I think I know the answer to) and time their response, and subjectively judge their output. For the question I&amp;rsquo;ve chosen:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Thinking about Docker, what&amp;rsquo;s the difference between [CMD] and [EntryPoint]?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This seems like a fairly specific bit of knowledge someone might want to know about, I know the answer, and the first page of google results are mostly good so there should be sufficient training data. I&amp;rsquo;ve put both terms in square brackets as a red herring, and same with the camelcase for ENTRYPOINT. I also didn&amp;rsquo;t specify that these are both usually defined in the dockerfile. I&amp;rsquo;ve had a go at the same question, and &lt;a href="https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/"&gt;published it here last week&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="the-results"&gt;The results&lt;/h3&gt;
&lt;p&gt;According to me, I am the winner.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/2309065-t2sarahconnor2.jpg" width="640" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-2.37.43-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The word count for my answer would be a bit higher if we counted the text in my images, which we probably should. I made up my times by guessing what they&amp;rsquo;d be if you asked me this question.&lt;/p&gt;
&lt;h3 id="the-contestants"&gt;The contestants&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;codeqwen&lt;/em&gt; and &lt;a href="https://chat.deepseek.com/coder"&gt;&lt;em&gt;deepseek-coder&lt;/em&gt;&lt;/a&gt; are both optimised for chatting about code, which I&amp;rsquo;m claiming Docker skills are a legitimate part of. They both also do autocomplete, and I&amp;rsquo;m using &lt;em&gt;codeqwen&lt;/em&gt; for that in VSCode. &lt;em&gt;deepseek-coder&lt;/em&gt; is about twice as big, and you&amp;rsquo;d think better, which it was, but in my opinion, only a little. codeqwen had a clear error and &lt;em&gt;deepseek-coder&lt;/em&gt; was a bit muddled in some parts but did a great job of wrapping it up with an explanation of where you&amp;rsquo;d use both.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;phi3&lt;/em&gt;&amp;rsquo;s is small (half the size of most of the others here) and great for chatting. For general questions it&amp;rsquo;s very impressive for it&amp;rsquo;s size, but was useless for this task. It&amp;rsquo;s interesting to me that the smartest and the stupidest AI&amp;rsquo;s had the most to say, and that my explanation was almost the exact size of all the others.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/d11c1d71-92aa-43dd-9b44-39e7ac1b2727_1600x900.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;dolphin-mistral&lt;/em&gt;&amp;rsquo;s claim to fame is that it&amp;rsquo;s uncensored. So if you ask it how to build an improvised explosive, overturn an election, or trick a co-worker into falling in love with you, it will happily tell you - something the other models here cannot. Basically, it laughs at the first law of robotics. Even though launching a Docker container is not illegal or unethical, it had a reasonable, usable answer for our question.&lt;/p&gt;
&lt;p&gt;I tried two versions of &lt;em&gt;llama3&lt;/em&gt;. To explain the difference, we need to go into an explanation of how large language models work, which I don&amp;rsquo;t know, so I&amp;rsquo;m just going to hallucinate it for you:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/input.jpg" width="800" alt=""&gt;
&lt;p&gt;If you vacuum up heaps of input (the training data), then filter out all the cruft (&amp;rsquo;the&amp;rsquo;, &amp;lsquo;a&amp;rsquo;, &amp;rsquo;not)&amp;rsquo;, then put it into a special multidimensional database so that similar things are near each other (eg &amp;lsquo;rose&amp;rsquo; is near &amp;lsquo;flower&amp;rsquo;, &amp;lsquo;red&amp;rsquo; and &amp;rsquo;titanic&amp;rsquo; and a long way from &amp;lsquo;bulldozer&amp;rsquo; and &amp;lsquo;antidisestablishmentarianism&amp;rsquo;) and the database also includes how far away from each other those things are, then it is very, very, big. Too big to put on my MacBook.&lt;/p&gt;
&lt;p&gt;We can reduce the size of it by &amp;lsquo;quantising&amp;rsquo; it which is a word I&amp;rsquo;ve heard on a podcast and might mean reducing the resolution of the numbers representing the distances between concepts in the database. This is the &amp;lsquo;q8&amp;rsquo; and &amp;lsquo;q4&amp;rsquo; you can see in the tags in the table. &amp;lsquo;q4&amp;rsquo; is going to be smaller, but less accurate than a &amp;lsquo;q8&amp;rsquo; of the same data.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the situation with these two versions of &lt;em&gt;llama3&lt;/em&gt; - one is more &amp;lsquo;compressed&amp;rsquo;. The relationship between the quantitation and the usefulness of the model is not linear for many applications, and that seems to be the case here. The bigger model did produce some more detail, but I actually preferred the output of the smaller one.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;It would be foolish to put much weight on a conclusion from a single run of a dubious test analyzed by a subjective carbon based lifeform, but anyway&amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of these models produced a useful starting point except &lt;em&gt;phi3&lt;/em&gt;. You probably could have just used what the others produced and gone on working with your dockerfile and things would have worked out fine.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;llama3&lt;/em&gt;&amp;rsquo;s performance matches my experience of other times I&amp;rsquo;ve been using it. It&amp;rsquo;s just pretty great for what it is.&lt;/li&gt;
&lt;li&gt;Most of the explanations over-complicated things - this probably could have been fixed with a better prompt.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s sort of magic when you think this is most of the world&amp;rsquo;s knowledge squashed into 4GB on my laptop in a form I can just ask questions of&lt;/li&gt;
&lt;li&gt;There is room for improvement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s easy to imagine that if these models were able to reach out to the internet and check what they&amp;rsquo;d come up with then generate a response by combining their first guess and their new knowledge, they&amp;rsquo;d be a lot better. That&amp;rsquo;s basically what &lt;a href="https://www.perplexity.ai/search/thinking-about-docker-what-s-t-9wJXPl_iTv2BLE60.QXgGA"&gt;Perplexity&lt;/a&gt; does, and it&amp;rsquo;s output is better than any of my local models, and it includes some of the links it used which would probably clear up any further questions. That sort of functionality is not far away for local models, and something like it is running in &lt;a href="https://useanything.com/"&gt;AnythingLLM&lt;/a&gt;, so I expect these will be indispensable tools in a year.&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM debian:stable-slim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;doc-cmd&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM debian:stable-slim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;echo&amp;#34;, &amp;#34;Hello World&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM debian:stable-slim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;&amp;amp; Hello World from CMD&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Save the environment variable IMAGE_URL into&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# a file for later use in the cron script&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;env &lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; grep IMAGE_URL &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; image_url&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then in my script that is run by cron, I reconstitute it from the file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Read the file saved by the entry point script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# and extract the environment variable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;while&lt;/span&gt; IFS&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;=&amp;#39;&lt;/span&gt; read &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;r key value&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[[&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;key &lt;span style="color:#81a1c1"&gt;==&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;IMAGE_URL&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;]];&lt;/span&gt; then
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;export&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$key=$value&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;done &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/image_url.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>Outputting to the console, in Docker, from a cron job</title><link>https://blog.iankulin.com/outputting-to-the-console-in-docker-from-a-cron-job/</link><pubDate>Mon, 08 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/outputting-to-the-console-in-docker-from-a-cron-job/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-3.48.02-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re googling this exact title, you&amp;rsquo;re probably bumping your head against the same things I was today. I was debugging a completely different project, and needed to print to the console, from a &lt;code&gt;cron&lt;/code&gt; job, in a Docker container. Turns out this isn&amp;rsquo;t as straightforward as I thought.&lt;/p&gt;
&lt;h3 id="foreground-cron"&gt;Foreground cron&lt;/h3&gt;
&lt;p&gt;Before you even get to the problem space, here&amp;rsquo;s a tip. If you want to have a cron job running in a container, start &lt;code&gt;cron&lt;/code&gt; in the foreground. If you do not, Docker realises nothing is going on, and exits. If you want to keep the container active so your &lt;code&gt;cron&lt;/code&gt; jobs get a chance to execute, then start it in the foreground.&lt;/p&gt;
&lt;h3 id="stdout"&gt;stdout&lt;/h3&gt;
&lt;p&gt;I always think about &lt;code&gt;stdout&lt;/code&gt; as being the console, but I guess at one time it was probably a teletype printer, and for &lt;code&gt;cron&lt;/code&gt;, &lt;a href="https://askubuntu.com/questions/1454389/where-does-the-users-cron-output-go-to-by-default-on-ubuntu"&gt;apparently it&amp;rsquo;s an email to the user&lt;/a&gt;. So me directing the output of the command I&amp;rsquo;m running in crontab to stdout is not helpful. It turns out you need something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* * * * * /script.sh &amp;gt; /proc/1/fd/1 2&amp;gt;&amp;amp;1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://github.com/moby/moby/issues/19616#issuecomment-174492543"&gt;There&amp;rsquo;s some good Linuxy reason for this&lt;/a&gt; - &lt;code&gt;/proc/1/fd/1&lt;/code&gt; is the standard output of a particular process which happens to be the process for the entry point of the container or some such thing,&lt;/p&gt;
&lt;p&gt;If you are not familiar with &lt;code&gt;cron&lt;/code&gt;, there are going to be much better explanations than this, but it&amp;rsquo;s a mechanism for running jobs at various times. It uses a file, called the &lt;code&gt;crontab&lt;/code&gt; to define these. Each of the asterisks above are spots where we can define the minute, hour, day, etc that the job runs by entering a number. If they are all asterisks then we are saying &amp;lsquo;run this every minute&amp;rsquo;. Following that is just the command, which in this case is run &lt;code&gt;/script.sh&lt;/code&gt; and send the output and error output to this proc file which happens to be the console.&lt;/p&gt;
&lt;p&gt;Other gotchas when working with cron is which user it&amp;rsquo;s running as (so permission problems) and where it&amp;rsquo;s running from (don&amp;rsquo;t use relative file paths).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/cron-docker-output"&gt;Example project on GutHub&lt;/a&gt;&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; forgejo:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; external: false
&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;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; server:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: codeberg.org/forgejo/forgejo:7.0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - USER_UID=112
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - USER_GID=103
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: always
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - forgejo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./forgejo:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /etc/timezone:/etc/timezone:ro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /etc/localtime:/etc/localtime:ro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#39;80:3000&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#39;2200:22&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM node:20
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /usr/src/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY package*.json ./
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN npm install
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY . .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE 3000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;db
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;node_modules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.vscode
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.dockerignore
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.gitignore
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run -it iankulin/tick /bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;db
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;node_modules
&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;dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>Virtual Hosts on "Static Web Server"</title><link>https://blog.iankulin.com/virtual-hosts-on-static-web-server/</link><pubDate>Mon, 22 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/virtual-hosts-on-static-web-server/</guid><description>&lt;p&gt;I&amp;rsquo;ve been running &lt;a href="https://blog.iankulin.com/nginx-proxy-manager/"&gt;NGINX Proxy Manager&lt;/a&gt; (NPM) in my homelab for a bit, and I&amp;rsquo;ve been meaning to clean up the VPS that runs most of my websites and public facing servers, so I&amp;rsquo;m considering running NGINX Proxy Manager on that VPS. While NGINX Proxy Manager wraps up the configs in a beautiful GUI, in the process you lose some of NGINXs capabilities. In particular there&amp;rsquo;s no GUI way to serve static virtual hosts from NGINX Proxy Manager.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a pity, since it seems like it wouldn&amp;rsquo;t be a lot of work, but in any case we can easily just run another web server and proxy to it. I guess I could run another instance of NGINX, but on $5 VPSs memory is a bit scarce, and since my sites are extremely low traffic, perhaps something a bit lighter is in order.&lt;/p&gt;
&lt;p&gt;An option mentioned in several posts for this exact situation is the very well named &lt;a href="https://static-web-server.net/"&gt;Static Web Server&lt;/a&gt;. It&amp;rsquo;s written in Rust, the docker image is less that 10MB, and it claims to be able to serve for virtual hosts. Let&amp;rsquo;s give it a try.&lt;/p&gt;
&lt;h3 id="directory-structure"&gt;Directory Structure&lt;/h3&gt;
&lt;p&gt;My routine setup now is that everything runs in docker. There&amp;rsquo;s a directory under the home directory of my user for named for the container which holds the &lt;code&gt;docker-compose.yml&lt;/code&gt;. Underneath that there&amp;rsquo;s two sub-directories: &lt;code&gt;data&lt;/code&gt; - which holds all the app data, and &lt;code&gt;config&lt;/code&gt; - which holds the app settings files.&lt;/p&gt;
&lt;p&gt;In the data directory, I want to have sub-directories for each virtual host, then inside them a &lt;code&gt;public&lt;/code&gt; directory which will hold the files to be served. This will allow me to keep config or other files for each virtual host in the directory above &lt;code&gt;public&lt;/code&gt; which should not be accessible. That seems like a good arrangement so I can manage each virtual host in git and pull the sites down from a repository into their directory without things like the &lt;code&gt;.gitignore&lt;/code&gt; being exposed to the world.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-8.15.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-8.15.14-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running Tailscale on this VM, so it can be referred to by any of the addresses &lt;code&gt;100.124.218.26&lt;/code&gt;, &lt;code&gt;192.168.100.35&lt;/code&gt; or &lt;code&gt;ct357-sws&lt;/code&gt;. These are going to be stand- ins for the different virtual host names. The index.html files you can see are all different; I&amp;rsquo;ve edited them so each one just outputs the name of the directory it is being served from. eg:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-9.20.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-9.20.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="docker"&gt;Docker&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;3.3&amp;#34;&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;services&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; website&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; joseluisq&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;static&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;web&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;server&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;sws&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; unless&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; SERVER_CONFIG_FILE&lt;span style="color:#81a1c1"&gt;=/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;toml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;toml&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;toml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;re probably familiar with docker-compose by now, but if not, the volumes lines probably need explaining. They map &amp;lsquo;real-world&amp;rsquo; directories on our server, to the internal directories &lt;em&gt;inside&lt;/em&gt; the container. This is a useful thing - we could copy our files into the container but those changes would be ephemeral, they&amp;rsquo;d be gone next time we restart the container. By creating these links, we can store the web server configs and data on our server, but from the point of view of the app, they appear inside.&lt;/p&gt;
&lt;p&gt;An example will make this more concrete, lets look at the first line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is saying that the directory inside the container named &lt;code&gt;/var&lt;/code&gt; is mapped onto the external directory &lt;code&gt;./data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Consider our file in the tree listing above &lt;code&gt;~/sws/data/100.124.218.26/public/index.html&lt;/code&gt; the web server running inside the container will see that file at &lt;code&gt;/var/100.124.218.26/public/index.html&lt;/code&gt; It seems weird at first, but you soon get used to this sort of container directory mapping maths.&lt;/p&gt;
&lt;h3 id="config"&gt;Config&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s excellent documentation about Static Web Server on their &lt;a href="https://static-web-server.net/"&gt;web site&lt;/a&gt;, so I&amp;rsquo;m not going to go through the whole &lt;code&gt;config.toml&lt;/code&gt; file, in any case, I&amp;rsquo;ve left nearly everything on the defaults. Instead, lets scroll down to the bottom and look at the virtual hosts settings.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;[[&lt;/span&gt;advanced&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;virtual&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;hosts&lt;span style="color:#eceff4"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;100.124.218.26&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/100.124.218.26/public&amp;#34;&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:#eceff4"&gt;[[&lt;/span&gt;advanced&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;virtual&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;hosts&lt;span style="color:#eceff4"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ct357-sws&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/ct357-sws/public&amp;#34;&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:#eceff4"&gt;[[&lt;/span&gt;advanced&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;virtual&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;hosts&lt;span style="color:#eceff4"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;192.168.100.35&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/192.168.100.35/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="it-works"&gt;It works&lt;/h3&gt;
&lt;p&gt;A quick &lt;code&gt;docker compose up -d&lt;/code&gt;, and we&amp;rsquo;re in business.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-21-at-8.52.14-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-21-at-8.57.27-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Due Diligence on a Docker Image</title><link>https://blog.iankulin.com/due-diligence-on-a-docker-image/</link><pubDate>Mon, 08 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/due-diligence-on-a-docker-image/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/gray-figure-ELLDKLrXMoA"&gt;&lt;img src="https://blog.iankulin.com/images/brett-jordan-elldklrxmoa-unsplash.jpg" width="640" alt=""&gt;&lt;/a&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@brett_jordan?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Brett Jordan&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/gray-figure-ELLDKLrXMoA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I need a survey tool, and a quick search turned up &lt;a href="https://www.limesurvey.org/"&gt;LimeSurvey&lt;/a&gt;, there&amp;rsquo;s a &amp;lsquo;community edition&amp;rsquo; so naturally I plan to self-host it. I scrolled down to the &amp;lsquo;installation&amp;rsquo; section of the &lt;a href="https://manual.limesurvey.org/Installation_-_LimeSurvey_CE/en"&gt;manual&lt;/a&gt; which has a big list of PHP dependencies.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-7.20.31-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Ain&amp;rsquo;t nobody got the time for that in 2024, I scroll further looking for the docker-compose but there isn&amp;rsquo;t one. Huh. No official Docker image.&lt;/p&gt;
&lt;p&gt;Making my own docker image will be a little bit more work than just the pain of installing it manually and writing the Ansible playbook, but almost certainly someone else will have done that for us, so pop over to Docker Hub to have a look.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-7.38.06-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to want the &amp;rsquo;trusted content&amp;rsquo; right?&lt;/p&gt;
&lt;h3 id="who-do-you-trust"&gt;Who do you trust?&lt;/h3&gt;
&lt;p&gt;At some stage when you run software, you are going to need to decide who you trust for this application. You might say &amp;ldquo;no, as an Open Source advocate, I&amp;rsquo;m going to read through the code and compile it myself&amp;rdquo; - and that&amp;rsquo;s going to be a legit approach in some circumstances. But of course you&amp;rsquo;re still choosing to trust all the libraries it uses, the compiler, the operating system, and the computer chipset (or worse - your hosting provider).&lt;/p&gt;
&lt;p&gt;If you want to go hardcore secure, you&amp;rsquo;ll eventually find yourself constructing your own &lt;code&gt;[fab](https://en.wikipedia.org/wiki/Semiconductor_fabrication_plant)&lt;/code&gt;. This is the concept of the Security-Convenience trade-off. More security generally equals less convenience and vice versa.&lt;/p&gt;
&lt;p&gt;To make a sensible decision about this when choosing software I think about the threat profile, and have some rough metrics about what makes me more comfortable or less comfortable about a software artifact.&lt;/p&gt;
&lt;p&gt;The reason this is a front-of-mind issue for me is that software packaging is an easy stage for bad actors to insert their code. We&amp;rsquo;ve seen a &lt;a href="https://popey.com/blog/2024/02/exodus-bitcoin-wallet-490k-swindle/"&gt;bit of that over the past month&lt;/a&gt; in the Canonical Snap store. Nefarious bitcoin stealers were packaging their software as legit, users were downloading them and installing them and one cryptobro lost a heap of money. It&amp;rsquo;s easy to imagine lots of related exploits - for example just altering the original code and packaging it. In the case of LimeSurvey (which is used by many researchers at leading universities perhaps a bad actor might want to be able to run bots from inside verifiable uni IP addresses.&lt;/p&gt;
&lt;h3 id="trust-factors"&gt;Trust Factors&lt;/h3&gt;
&lt;p&gt;What are the things that might reduce our anxiety about the software bill of materials we&amp;rsquo;re dealing with? These are just mine - a complete non-expert:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trusted organisation - If the Ubuntu team have signed off on something, I&amp;rsquo;m going to trust it more that some other group I never heard of till today.&lt;/li&gt;
&lt;li&gt;Popular - it&amp;rsquo;s not just it&amp;rsquo;s reassuring that other people have decided it&amp;rsquo;s trustworthy, it&amp;rsquo;s also a comforting idea that if there is an exploit, it&amp;rsquo;s more likely to be discovered, and to hurt someone else first. If there&amp;rsquo;s a zero day exploit in MacOS I&amp;rsquo;ll hear about it on Mastodon and be able to get some advice about a work-around or fix.&lt;/li&gt;
&lt;li&gt;Updated - a project under current development is more likely to be applying updates to any compromised libraries, and there&amp;rsquo;s probably a community of some sort where security concerns are considered.&lt;/li&gt;
&lt;li&gt;Verifiable - One of the big strengths of Open Source. Even if I don&amp;rsquo;t have the skills or time to read the code, there&amp;rsquo;s a good chance that other people are. And in the case of a container image, I actually do sort of have the skills to read the dockerfile and know what&amp;rsquo;s gone into it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="choosing-an-image"&gt;Choosing an Image&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s apply some of these rules-of-thumb to my LimeSurvey container needs.&lt;/p&gt;
&lt;p&gt;When Docker Hub says something is &amp;ldquo;trusted content&amp;rdquo; they&amp;rsquo;ve already done some of my work for me. And when I click through to &lt;a href="https://hub.docker.com/r/eucm/limesurvey"&gt;eucm/limesurvey&lt;/a&gt; it says that the developer (eucm) is a &amp;ldquo;&lt;a href="https://docs.docker.com/trusted-content/dsos-program/#:~:text=The%20Docker-Sponsored%20Open%20Source,Insights%20and%20analytics"&gt;Sponsored OSS&lt;/a&gt;&amp;rdquo; which means a human at Docker thinks this developer group are legit. These are all encouraging factors.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-9.47.41-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-9.47.41-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s less encouraging is &amp;ldquo;Pulls 635&amp;rdquo; which does not seem a lot, and &amp;ldquo;Updated over 3 years ago&amp;rdquo;. Also I&amp;rsquo;ve got no reason to think this is an official image - eucm doesn&amp;rsquo;t seem to have any connection to the LimeSurvey developers. Let&amp;rsquo;s look at the other images.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-10.25.21-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-10.25.21-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s more further down but all with about 100K pulls or less. Of these four, we&amp;rsquo;ve already eliminated the top one, and we can probably eliminate the bottom one based on it&amp;rsquo;s age - although it&amp;rsquo;s always interesting to see an image with so many pulls and no updates since that sometimes happens when a project changes hands or gets relisted under a different owner.&lt;/p&gt;
&lt;p&gt;Either of the other two (acspri/limesurvey &amp;amp; martialblog/limesurvey) are worth investigating - there&amp;rsquo;s nothing much to tell them apart in this view since they both have about the same number of stars (the number next to the pulls number). I&amp;rsquo;ll start at the top.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-11.34.52-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-29-at-11.34.52-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s promising to have a good readme, including a docker-compose example, but there&amp;rsquo;s no github link, and I really want a peek at the dockerfile. If I click through to the developer, they seem to be the &lt;a href="https://www.acspri.org.au"&gt;Australia Consortium for Social and Political Research Inc&lt;/a&gt; - which it makes sense why they would be interested in a survey tool. Additionally, they seem to have an &lt;a href="https://www.acspri.org.au/limesurvey"&gt;official link to the project&lt;/a&gt;. They do have an active GitHub account, but it doesn&amp;rsquo;t include a repository for this which seems, odd.&lt;/p&gt;
&lt;p&gt;However, there is this repo &lt;a href="https://github.com/adamzammit/limesurvey-docker"&gt;adamzammit / limesurvey-docker&lt;/a&gt; which appears to get pushed to it&amp;rsquo;s own &lt;a href="https://hub.docker.com/r/adamzammit/limesurvey"&gt;dockerhub&lt;/a&gt; as well as the ACSPRI one. There is a note on the adamzamit dockerhub saying this used to be the acspri but has been moved here - that would be more convincing if the note was on the acspri dockerhub. The github looks legit, and there was nothing suspicious (to my inexpert view) in the dockerfile.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-03-30-at-11.33.52-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-30-at-11.33.52-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://hub.docker.com/u/martialblog"&gt;martialblog/limesurvey&lt;/a&gt; one also looks good - recently updated, lots of pulls, a comprehensive readme, and a github link right at the top. The only criteria it doesn&amp;rsquo;t meet is &amp;ldquo;trusted organisation&amp;rdquo; but the link to an active github goes part of the way. I&amp;rsquo;d be happy to use this container for the purpose I&amp;rsquo;ve got in mind.&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Use an official Node.js runtime as the base image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM node:20-alpine
&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;# Set the working directory in the container
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /usr/src/app
&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;# Copy package.json and package-lock.json to the working directory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY package*.json .
&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;RUN npm install
&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;# Copy the rest of the application source code to the container
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY ./server.js .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY ./LICENSE .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY ./readme.md .
&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;# Expose the port that the Node.js app will listen on
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE 3000
&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;# Define the command to start your Node.js app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [ &amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34; ]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mdserver:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: iankulin/mdserver:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#34;3000:3000&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./public:/usr/src/app/public 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services: 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;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM busyboxRUN mkdir /appCOPY script.sh /app/script.shWORKDIR /appRUN chmod +x script.shCMD [&amp;#34;./script.sh&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;script.sh&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/shecho &amp;#34;Hello from Docker!&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker build -t hello-docker .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker run hello-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker tag hello-docker:latest ct390-docker-reg:5000/hello-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push ct390-docker-reg:5000/hello-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://ct390-docker-reg:5000/v2/_catalog
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#a3be8c"&gt;&amp;#39;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}&amp;#39;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; sudo tee &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;docker&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;daemon&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;jsonsudo systemctl daemon&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;reloadsudo systemctl restart docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>ViewTube</title><link>https://blog.iankulin.com/viewtube/</link><pubDate>Mon, 27 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/viewtube/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.17.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Whenever I encounter one of those &amp;ldquo;What are you self-hosting?&amp;rdquo; threads, I know I&amp;rsquo;m about to waste an hour looking at, and often trying out, software I probably don&amp;rsquo;t really need, and that was the case with &lt;a href="https://lemmy.world/post/8385160"&gt;this post&lt;/a&gt; on the &lt;a href="https://lemmy.world/c/selfhost@lemmy.ml"&gt;lemmy.world Selfhosted&lt;/a&gt; community.&lt;/p&gt;
&lt;p&gt;The basic idea of ViewTube is that it&amp;rsquo;s a self-hosted front end for YouTube, which just happens to strip out all the advertising and tracking. You can create your own local accounts which allows you to subscribe to channels and which keeps your progress so you don&amp;rsquo;t start over if you go back to a video - although I couldn&amp;rsquo;t see a history list. Forgetting your history might be a feature in an app designed to prevent tracking.&lt;/p&gt;
&lt;p&gt;It only took five minutes to get it running to try out, and most of that was downloading the docker images. I just made a directory in a VM and dropped this docker compose into it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3&amp;#39;
&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;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; viewtube:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # Or use mauriceo/viewtube:dev for the development version
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: mauriceo/viewtube:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # ViewTube will not start until the database and redis are ready
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; depends_on:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube-mongodb
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube-redis
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # Make sure all services are in the same network
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # This will map ViewTube&amp;#39;s data directory to the local folder ./data/viewtube/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/viewtube:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - VIEWTUBE_DATABASE_HOST=viewtube-mongodb
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - VIEWTUBE_REDIS_HOST=viewtube-redis
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - 8066:8066
&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; viewtube-mongodb:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: mongo:4.4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/db:/data/db
&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; viewtube-redis:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: redis:7
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/redis:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; viewtube:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only change in here from the &lt;a href="https://viewtube.wiki/installation/docker"&gt;official one&lt;/a&gt; was to change to an older version since I hadn&amp;rsquo;t passed through the CPU in host mode, so there was no &lt;a href="https://old.reddit.com/r/homelab/comments/yvo4jm/how_do_i_enable_avx_on_my_server/"&gt;AVX support which is required by newer versions&lt;/a&gt; of MongoDB.&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Extract the version number from package.json using jq&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VERSION&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;$(&lt;/span&gt;jq -r .version package.json&lt;span style="color:#81a1c1;font-weight:bold"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build --platform linux/amd64 -t iankulin/mdserver:$VERSION -t iankulin/mdserver:latest .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build --platform linux/arm64 -t iankulin/mdserver:arm64-$VERSION -t iankulin/mdserver:arm64-latest .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push iankulin/mdserver:arm64-$VERSION 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push iankulin/mdserver:arm64-latest 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push iankulin/mdserver:$VERSION
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push iankulin/mdserver:latest 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx create --use
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then to create my dual architecture image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx build --push \
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;--platform linux/arm64,linux/amd64 \
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-t iankulin/mdserver:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker buildx build --push \
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;--platform linux/arm64,linux/amd64,linux/arm/v7 \
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-t iankulin/mdserver:latest .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3.8&amp;#39;
&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;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uptime-kuma:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: louislam/uptime-kuma:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: uptime-kuma
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - uptime-kuma:/app/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#34;3001:3001&amp;#34; # &amp;lt;Host Port&amp;gt;:&amp;lt;Container Port&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: always
&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;volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uptime-kuma:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Mounts&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Type&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;volume&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;uptimekuma_uptime-kuma&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Source&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/lib/docker/volumes/uptimekuma_uptime-kuma/_data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Destination&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/app/data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Driver&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;local&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Mode&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;z&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;RW&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Propagation&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /app/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;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;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3: docker0: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc noqueue state DOWN group default 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;history | grep &amp;#34;docker run&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Mounts&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Type&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;volume&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;jellyfin-config&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Source&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/lib/docker/volumes/jellyfin-config/_data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Destination&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/config&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Driver&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;local&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Mode&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;z&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;RW&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Propagation&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Type&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;bind&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Source&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/mnt/media/video&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Destination&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/media&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Mode&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;RW&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Propagation&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;rprivate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Type&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;volume&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;jellyfin-cache&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Source&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/lib/docker/volumes/jellyfin-cache/_data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Destination&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/cache&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Driver&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;local&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Mode&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;z&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;RW&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Propagation&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;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>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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat domain.cert.pem intermediate.cert.pem &amp;gt; fullchain.pem
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#b48ead"&gt;2023&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#b48ead"&gt;04&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#b48ead"&gt;21&lt;/span&gt; &lt;span style="color:#b48ead"&gt;05&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;42&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;45&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;emerg&lt;span style="color:#eceff4"&gt;]&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#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)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nginx&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;emerg&lt;span style="color:#eceff4"&gt;]&lt;/span&gt; cannot &lt;span style="color:#81a1c1"&gt;load&lt;/span&gt; certificate &lt;span style="color:#a3be8c"&gt;&amp;#34;/etc/nginx/conf.d/fullchain.pem&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; PEM_read_bio_X509&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; failed &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;SSL&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; error&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0908&lt;/span&gt;F066&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;PEM routines&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;get_header_and_data&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;bad end line&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run -p 80:80 -d -v ~/www:/usr/share/nginx/html nginx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#34;3.9&amp;#34;
&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;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; client:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: nginx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: nginx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - 80:80
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - 443:443
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /home/ian/iankulin.com/www:/usr/share/nginx/html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /home/ian/iankulin.com/nginx/conf/:/etc/nginx/conf.d/:ro
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: always
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; listen 80 default_server;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; listen [::]:80 default_server;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; root /usr/share/nginx/html;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; server_name iankulin.com www.iankulin.com;
&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; listen 443 ssl; 
&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; # RSA certificate
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ssl_certificate /etc/nginx/conf.d/fullchain.pem; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ssl_certificate_key /etc/nginx/conf.d/private.key.pem; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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>Where Do Docker Container Logs Go?</title><link>https://blog.iankulin.com/where-do-docker-container-logs-go/</link><pubDate>Sat, 08 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/where-do-docker-container-logs-go/</guid><description>&lt;p&gt;I&amp;rsquo;m still loving the Docker &amp;ldquo;just works&amp;rdquo; magic, despite their &lt;a href="https://www.theregister.com/2023/03/17/docker_free_teams_plan/"&gt;terrible PR skills&lt;/a&gt;, but sometimes I start a container, then the &lt;code&gt;docker ps -a&lt;/code&gt; shows it exited almost immediately. Clearly I&amp;rsquo;ve made a mistake, but there&amp;rsquo;s no stdout error message to tell me what I&amp;rsquo;ve done wrong, where is it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at an example from today. I&amp;rsquo;m testing &lt;a href="https://filebrowser.org/"&gt;Filebrowser&lt;/a&gt; on a dev machine before I deploy it to the remote backup machine I&amp;rsquo;m assembling. And instead of following the &lt;a href="https://filebrowser.org/installation"&gt;official instructions&lt;/a&gt;, I&amp;rsquo;m following a &lt;a href="https://bobcares.com/blog/filebrowser-installation-in-docker/"&gt;blog post&lt;/a&gt; which has a few more details, but unfortunately also a small error.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-02-at-1.35.16-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-02-at-1.35.16-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The first sign of a problem is that the container is not running after I&amp;rsquo;ve launched it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-02-at-1.42.09-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;To cat the log for the exited container is simple. Note that the (randomly provided) name for the container is &lt;code&gt;eager_haslett&lt;/code&gt; so to see the log we enter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker logs eager_haslett
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The log output was:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;panic: While parsing config: invalid character &amp;#39;\n&amp;#39; in string literal
&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;goroutine 1 [running]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;github.com/filebrowser/filebrowser/v2/cmd.initConfig()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/work/filebrowser/filebrowser/cmd/root.go:410 +0x346
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;github.com/spf13/cobra.(*Command).preRun(...)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:886
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;github.com/spf13/cobra.(*Command).execute(0x1828580, {0xc000030220, 0x0, 0x0})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:822 +0x44e
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;github.com/spf13/cobra.(*Command).ExecuteC(0x1828580)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:974 +0x3b4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;github.com/spf13/cobra.(*Command).Execute(...)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.4.0/command.go:902
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;github.com/filebrowser/filebrowser/v2/cmd.Execute()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/work/filebrowser/filebrowser/cmd/cmd.go:9 +0x25
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;main.main()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	/home/runner/work/filebrowser/filebrowser/main.go:8 +0x17
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s our answer right at the top of the log - there&amp;rsquo;s a newline character in the middle of a key:value pair in the config JSON. If you look at the blog page above you can see it after the database.db&lt;/p&gt;</description></item></channel></rss>