{"id":2804,"date":"2025-10-22T18:32:44","date_gmt":"2025-10-22T17:32:44","guid":{"rendered":"https:\/\/andylockran.dev\/?p=2804"},"modified":"2025-10-22T19:26:16","modified_gmt":"2025-10-22T18:26:16","slug":"how-do-you-keep-your-dockerfile-from-sha-tags-up-to-date","status":"publish","type":"post","link":"https:\/\/andylockran.dev\/index.php\/2025\/10\/22\/how-do-you-keep-your-dockerfile-from-sha-tags-up-to-date\/","title":{"rendered":"How do you keep your Dockerfile FROM sha tags up to date?"},"content":{"rendered":"\n<p>For my trusty followers who have been crying out for a technology-based post &#8211; TODAY IS THE DAY.<\/p>\n\n\n\n<p>Due to recent supply chain attacks; more platforms are going down the route of suggesting sha-pinning dependencies<\/p>\n\n\n\n<p>GitHub has a flag that ensures that all your<a href=\"https:\/\/github.blog\/changelog\/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions\/\"> GitHub Action dependencies are explicitly tied to a sha<\/a>, instead of just a version tag (e.g. @v1)<\/p>\n\n\n\n<p>This helps to put you in control of version upgrades, as each time the author releases a new version.<\/p>\n\n\n\n<p>In the npm world, your package.json file defines the &#8216;range&#8217; of dependencies that you &#8216;directly&#8217; depend on, and the package-lock.json is a reference to all of the reified dependency tree versions that were pulled when you &#8216;built&#8217; the package. <\/p>\n\n\n\n<p>This produced repeatable builds (when using `npm ci`), as it only ever downloaded the dependencies &#8216;pinned&#8217; by the lockfile.  As npm follows SemanticVersioning, when new packages are updated, the dependency tree is re-worked out, and dependabot can make suggestions for how to update your package-lock.json file to satisfy both generic updates, and security patches.<\/p>\n\n\n\n<p>It&#8217;s great for npm (SemVer), it&#8217;s great for GHA (SemVer), but it&#8217;s also capable of supporting Dockerfiles.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FROM node:20-alpine<\/code><\/pre>\n\n\n\n<p>An innocuous Dockerfile, build using the Official Node image, and follows <a href=\"https:\/\/www.docker.com\/blog\/how-to-use-the-node-docker-official-image\/#Optimize-your-Node-image\">Docker&#8217;s own documentation<\/a> (from 2022).<\/p>\n\n\n\n<p>Way back in 2020, the first hit (for me) on Google is <a href=\"https:\/\/rockbag.medium.com\/why-you-should-pin-your-docker-images-with-sha-instead-of-tags-fd132443b8a6\" data-type=\"link\" data-id=\"https:\/\/rockbag.medium.com\/why-you-should-pin-your-docker-images-with-sha-instead-of-tags-fd132443b8a6\"><a href=\"https:\/\/rockbag.medium.com\/?source=post_page---byline--fd132443b8a6---------------------------------------\">B\u00e1lint Bir\u00f3<\/a><\/a>&#8216;s helpful blogpost on how to follow this approach for Docker images within the context of tools like docker-compose; and the official <a href=\"https:\/\/docs.docker.com\/reference\/dockerfile\/#from\">Dockerfile doc<\/a> shows how to do this using the FROM argument in a dockerfile:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FROM node:20-alpine@sha256:3bc9a4c4cc25cfde1e8f946341c85f333c36517aafda829b4bb7e785e9b5995c <\/code><\/pre>\n\n\n\n<p>Neat.<\/p>\n\n\n\n<p>However, the management of these images in DockerHub is kinda a different paradigm.  The office &#8216;node&#8217; image, of which <code>20-alpine<\/code> is just a label, has quite a load of other labels:<\/p>\n\n\n\n<p><a href=\"https:\/\/hub.docker.com\/_\/node\">https:\/\/hub.docker.com\/_\/node<\/a><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"610\" src=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image.png?resize=1024%2C610&#038;ssl=1\" alt=\"\" class=\"wp-image-2805\" srcset=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image.png?resize=1024%2C610&amp;ssl=1 1024w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image.png?resize=300%2C179&amp;ssl=1 300w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image.png?resize=768%2C457&amp;ssl=1 768w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image.png?w=1310&amp;ssl=1 1310w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>There are tens of labels, and the label I&#8217;ve targeted is mutable. It automatically keeps me up to date whenever the official maintainers release a new version.  However, it&#8217;s the same anti-pattern as the one that Github Actions recently fixed with their sha pinning.  <\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>So, if I want to maintain 20-alpine, and I want to pin to a sha, so that I only get an update if the maintainer posts a new version of the 20-alpine label to docker hub, that&#8217;s possible right?<\/p>\n<\/blockquote>\n\n\n\n<p>Well, I tested it and put a Dockerfile the following:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>FROM node:20-alpine@sha256:3bc9a4c4cc25cfde1e8f946341c85f333c3<\/code>6517aafda829b4bb7e785e9b5995c<\/code><\/pre>\n\n\n\n<p>and a dependabot.yml like so:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https:\/\/docs.github.com\/github\/administering-a-repository\/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\" # See documentation for possible values\n    directory: \"\/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"github-actions\" # See documentation for possible values\n    directory: \"\/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"docker\"\n    directory: \"\/\" # Location of package manifests\n    schedule:\n      interval: \"daily\"<\/code><\/pre>\n\n\n\n<p>and to my (un-surprise, as I&#8217;d read the docs), it didn&#8217;t quite work as anticipated, with the next PR suggesting the following change:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1012\" height=\"526\" src=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-1.png?resize=1012%2C526&#038;ssl=1\" alt=\"\" class=\"wp-image-2806\" srcset=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-1.png?w=1012&amp;ssl=1 1012w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-1.png?resize=300%2C156&amp;ssl=1 300w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-1.png?resize=768%2C399&amp;ssl=1 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Bump node from 20-alpine to 25-alpine !<\/p>\n\n\n\n<p>Now, node&#8217;s own guidance is as follows:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Major Node.js versions enter&nbsp;<em>Current<\/em>&nbsp;release status for six months, which gives library authors time to add support for them. After six months, odd-numbered releases (9, 11, etc.) become unsupported, and even-numbered releases (10, 12, etc.) move to&nbsp;<em>Active LTS<\/em>&nbsp;status and are ready for general use.&nbsp;<em>LTS<\/em>&nbsp;release status is &#8220;long-term support&#8221;, which typically guarantees that critical bugs will be fixed for a total of 30 months. Production applications should only use&nbsp;<em>Active LTS<\/em>&nbsp;or&nbsp;<em>Maintenance LTS<\/em>&nbsp;releases.<\/p>\n<\/blockquote>\n\n\n\n<p>So this PR is taking me from a current LTS release, in maintenace til April &#8217;26, to a current version that&#8217;s never going to enter LTS and not recommended for production use.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"500\" src=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-2.png?resize=960%2C500&#038;ssl=1\" alt=\"\" class=\"wp-image-2807\" srcset=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-2.png?w=960&amp;ssl=1 960w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-2.png?resize=300%2C156&amp;ssl=1 300w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-2.png?resize=768%2C400&amp;ssl=1 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><\/figure>\n\n\n\n<p>I think that it&#8217;s behaving like this because the implementation pattern that the official node maintains have taken to have loads of labels against a single image.  Dependabot for docker isn&#8217;t particularly smart, and looks for a temporally <code>later<\/code> tag against the <code>node<\/code> image, rather than using the <code>20-alpine<\/code> label to contextualise the sha and lock it against the label+sha as defined in my lovely Dockerfile.<\/p>\n\n\n\n<p>Now, no grief to the maintainers. The pattern that they support is a docker-hub image that<a href=\"https:\/\/github.com\/dependabot\/dependabot-core\/tree\/main\/docker\"> uses SemVer in the label to contextualize their dependencies,<\/a> so the nodeJS pattern is just one of many others that fall outside that paradigm. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A little bit more research&#8230;<\/h2>\n\n\n\n<p>After a quick Google, I went and found a few threads where people were cheering an alternative to dependabot for docker stuff, Renovate, by Mend.<\/p>\n\n\n\n<p>I&#8217;d come across this before, but being a Github native I&#8217;d always just deferred to dependabot.<\/p>\n\n\n\n<p>I had to install the app into my org, trust it with my test repo &#8211; but once it had been plumbed in and read my code.. It raised the PR that I wanted &#8211; a direct update of the sha for the node:20-alpine tag:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"216\" src=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-4.png?resize=1024%2C216&#038;ssl=1\" alt=\"\" class=\"wp-image-2812\" srcset=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-4.png?resize=1024%2C216&amp;ssl=1 1024w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-4.png?resize=300%2C63&amp;ssl=1 300w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-4.png?resize=768%2C162&amp;ssl=1 768w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-4.png?resize=1536%2C324&amp;ssl=1 1536w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-4.png?w=1887&amp;ssl=1 1887w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>I can see why Dependabot is struggling; but if renovate is ahead of the game here then I may be rethinking what else it&#8217;s picking up on that Dependabot is missing&#8230;<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"602\" src=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-3.png?resize=1024%2C602&#038;ssl=1\" alt=\"\" class=\"wp-image-2810\" srcset=\"https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-3.png?resize=1024%2C602&amp;ssl=1 1024w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-3.png?resize=300%2C176&amp;ssl=1 300w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-3.png?resize=768%2C451&amp;ssl=1 768w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-3.png?resize=1536%2C902&amp;ssl=1 1536w, https:\/\/i0.wp.com\/andylockran.dev\/wp-content\/uploads\/2025\/10\/image-3.png?resize=2048%2C1203&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>What do you use to manage your dependencies? Is there another dependency bot that&#8217;s even better?<\/p>\n<\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>For my trusty followers who have been crying out for a technology-based post &#8211; TODAY IS THE DAY. Due to recent supply chain attacks; more platforms are going down the route of suggesting sha-pinning dependencies GitHub has a flag that ensures that all your GitHub Action dependencies are explicitly tied to a sha, instead of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[1],"tags":[],"class_list":["post-2804","post","type-post","status-publish","format-standard","hentry","category-uncategorised"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack-related-posts":[],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/posts\/2804","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/comments?post=2804"}],"version-history":[{"count":3,"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/posts\/2804\/revisions"}],"predecessor-version":[{"id":2813,"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/posts\/2804\/revisions\/2813"}],"wp:attachment":[{"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/media?parent=2804"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/categories?post=2804"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/andylockran.dev\/index.php\/wp-json\/wp\/v2\/tags?post=2804"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}