<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ShipKit]]></title><description><![CDATA[Plain guides to GitHub Actions, Docker and CI problems, from ShipKit, which also sells GitHub Actions and Docker starter packs.]]></description><link>https://shipkit.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 11:43:17 GMT</lastBuildDate><atom:link href="https://shipkit.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Docker layers don't stay cached in GitHub Actions (and how type=gha works)]]></title><description><![CDATA[Your Docker build works in GitHub Actions, but the dependency install step runs from scratch on each push. Here's how the GitHub Actions cache for Docker…]]></description><link>https://shipkit.hashnode.dev/why-docker-layers-don-t-stay-cached-in-github-actions-and-how-type-gha-works</link><guid isPermaLink="true">https://shipkit.hashnode.dev/why-docker-layers-don-t-stay-cached-in-github-actions-and-how-type-gha-works</guid><category><![CDATA[Docker]]></category><category><![CDATA[GitHub Actions]]></category><category><![CDATA[Devops]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[ShipKit]]></dc:creator><pubDate>Mon, 26 Oct 2026 14:30:26 GMT</pubDate><content:encoded><![CDATA[<p>Your Docker build works in GitHub Actions, but the dependency install step runs from scratch on each push. Here's how the GitHub Actions cache for Docker layers works with Buildx and docker/build-push-action, and the usual reasons it misses. Each point comes from Docker's or GitHub's documentation (sources at the end).</p>
<h2>Why a CI build starts cold</h2>
<p>With the exception of single-CPU runners, each GitHub-hosted runner is a new virtual machine, so the layer cache from your last run isn't sitting on the machine. Docker's docs put it this way: CI environments "usually have little-to-no persistence between runs". BuildKit can export its build cache to an external location and import it in a later build, but the cache backends have to be exported to and imported from explicitly.</p>
<h2>The two lines that turn it on</h2>
<pre><code class="language-yaml">- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v4

- name: Build and push
  uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: &lt;registry&gt;/&lt;image&gt;:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max
</code></pre>
<ul>
<li><code>cache-to</code> exports the cache and <code>cache-from</code> imports it. <code>type=gha</code> stores it in the GitHub Actions cache.</li>
<li>With docker/build-push-action, the cache server URL and token are filled in for you. If you call <code>docker buildx build</code> from your own <code>run:</code> step, you have to expose those variables yourself.</li>
<li>setup-buildx-action uses the docker-container driver by default. The gha backend works with the plain <code>docker</code> driver only when the containerd image store is enabled.</li>
</ul>
<h2>What mode=max does</h2>
<p><code>mode</code> only applies to <code>cache-to</code>. In <code>mode=min</code> (the default), only layers that are exported into the resulting image are cached. In <code>mode=max</code>, layers of intermediate steps are cached too.</p>
<p>That matters for multi-stage builds. If your <code>npm ci</code> or <code>pip install</code> runs in an earlier stage whose layers don't end up in the final image, <code>mode=min</code> leaves them out of the cache, so the next run may have to install again. Docker's docs note the trade-off: a min cache is typically smaller, while a max cache is more likely to get more cache hits, and they suggest trying both.</p>
<h2>scope= when you build several images or branches</h2>
<p>The cache object is identified by a <code>scope</code>, which defaults to <code>buildkit</code>. If one workflow builds several images with the default scope, each build overwrites the cache of the previous one, leaving only the last one's cache. Give each image its own scope on both lines:</p>
<pre><code class="language-yaml">cache-from: type=gha,scope=api
cache-to: type=gha,mode=max,scope=api
</code></pre>
<p>Branches have their own boundary. A workflow run can restore caches from the current branch, the base branch and the default branch, but not from sibling or child branches. A cache created by a <code>pull_request</code> run belongs to the merge ref, so only re-runs of that pull request can restore it.</p>
<h2>Size and eviction limits</h2>
<p>From GitHub's dependency caching reference:</p>
<ul>
<li>Cache entries that haven't been accessed in over 7 days are removed.</li>
<li>The total cache size per repository is limited, 10 GB by default. A repository administrator can raise it, and usage beyond 10 GB is billed.</li>
<li>When a repository reaches its limit, GitHub deletes caches in order of last access date, oldest first. That can cause cache thrashing, where caches are created and deleted at a high rate.</li>
<li>Uploads are limited to 200 per minute per repository, and downloads to 1500 per minute.</li>
</ul>
<p>Docker notes that a <code>mode=min</code> cache is typically smaller, so a <code>mode=max</code> cache can use more of that space.</p>
<h2>Why the cache can still miss</h2>
<ol>
<li><strong>A layer above it changed.</strong> Once a layer changes, the layers after it rebuild too, even if they'd produce the same result. If <code>COPY . .</code> comes before your dependency install, a change to a source file invalidates the install layer. Docker's docs suggest copying the package files first, installing, then copying the rest of the project.</li>
<li><strong>mode=min in a multi-stage build.</strong> The install stage's layers aren't in the final image, so they weren't exported.</li>
<li><strong>Several images share one scope.</strong> The last build's cache replaced the others.</li>
<li><strong>Branch boundaries.</strong> A feature branch can't read a sibling branch's cache, and a pull request's cache stays with that pull request.</li>
<li><strong>The entry was evicted.</strong> It went unused for over 7 days, or the repository hit its size limit.</li>
<li><strong>The run had read-only cache access.</strong> Events such as <code>issue_comment</code> and <code>pull_request_target</code> get read-only cache access by default when they run in the default-branch context. They can restore, but <code>cache-to: type=gha</code> can fail with <code>failed to reserve cache</code>. Populate the cache from a workflow with write access, such as a <code>push</code> on the default branch. If export is optional, <code>ignore-error=true</code> on <code>cache-to</code> makes export failures non-fatal.</li>
<li><strong>You're relying on cache mounts.</strong> BuildKit doesn't preserve <code>RUN --mount=type=cache</code> mounts in the GitHub Actions cache by default.</li>
<li><strong>Old tooling on a self-hosted runner.</strong> As of April 15, 2025, only GitHub's cache service API v2 is supported. Docker lists Buildx v0.21.0 and BuildKit v0.20.0 as the minimum versions. On GitHub-hosted runners, build-push-action's Buildx and BuildKit are already up to date.</li>
</ol>
<h2>Where ShipKit's kit fits</h2>
<p>The docker-ghcr workflow in ShipKit's GitHub Actions + Docker Templates bundle uses the GitHub Actions cache for Docker layers (type=gha, mode=max). It doesn't set <code>scope=</code>, so if you add a second image to it, give each image its own scope as shown above. It's an example workflow to start from, so check it against the list above for your repository.</p>
<p>Sources (checked October 22, 2026): Docker Docs, "GitHub Actions cache" (cache backends); Docker Docs, "Cache storage backends"; Docker Docs, "Cache management with GitHub Actions"; Docker Docs, "Configuring your GitHub Actions builder"; Docker Docs, "Docker build cache"; Docker Docs, "Optimize cache usage in builds"; GitHub Docs, "Dependency caching reference"; GitHub Docs, "GitHub-hosted runners".</p>
<p>This post was drafted with AI tools and checked before it went out.</p>
<p>I make ShipKit, which gives away this guide and sells the paid <a href="https://8686702537545.gumroad.com/l/whlyio?utm_campaign=post2&amp;utm_medium=blog&amp;utm_source=hashnode">GitHub Actions + Docker Templates bundle</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Why your GitHub Actions schedule isn't firing: 9 checks]]></title><description><![CDATA[You added on: schedule, the cron looks right, and a manual run works, yet no scheduled run shows up. Work through these checks in order. Each one comes…]]></description><link>https://shipkit.hashnode.dev/why-your-github-actions-schedule-isn-t-firing-9-checks</link><guid isPermaLink="true">https://shipkit.hashnode.dev/why-your-github-actions-schedule-isn-t-firing-9-checks</guid><category><![CDATA[GitHub Actions]]></category><category><![CDATA[Devops]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[cron]]></category><dc:creator><![CDATA[ShipKit]]></dc:creator><pubDate>Thu, 22 Oct 2026 13:31:38 GMT</pubDate><content:encoded><![CDATA[<p>You added on: schedule, the cron looks right, and a manual run works, yet no scheduled run shows up. Work through these checks in order. Each one comes from GitHub's documentation (sources at the end). Some causes are on GitHub's side, so if your setup passes these checks, see the last section.</p>
<ol>
<li><p>Is the workflow file on the default branch? A schedule event only triggers a workflow if the workflow file exists on the default branch, and scheduled workflows run only on the default branch, using its latest commit. A cron block that exists only on a feature branch won't start a scheduled run. Merge it into the default branch (often main).</p>
</li>
<li><p>Is the workflow enabled? In a public repository, GitHub disables scheduled workflows automatically when no repository activity has occurred in 60 days. Open the Actions tab, select the workflow in the left sidebar, and look for an Enable workflow button, or run <code>gh workflow enable WORKFLOW</code>. A commit that changes the cron line, made by a user with write access, also reactivates a deactivated scheduled workflow, and that user becomes the actor for its runs.</p>
</li>
<li><p>Is this repository a fork? When a public repository is forked, its scheduled workflows are disabled by default, and workflows don't run in a fork until you enable GitHub Actions in the fork's Actions tab.</p>
</li>
<li><p>Did you write the time in UTC? Scheduled workflows run in UTC by default. <code>30 12 * * 2</code> means 12:30 UTC on Tuesday, not 12:30 in your local time. GitHub's docs also describe an optional timezone key that takes an IANA time zone name, such as America/New_York.</p>
</li>
<li><p>Is the cron expression one GitHub accepts? Use POSIX cron syntax with five fields (minute, hour, day of month, month, day of week) and the operators * , - and /. GitHub Actions doesn't support the shortcuts @yearly, @monthly, @weekly, @daily, @hourly or @reboot. The shortest interval is once per 5 minutes.</p>
</li>
<li><p>Is it scheduled at a busy time? The schedule event can be delayed when GitHub Actions is under high load, and the start of each hour is a high-load time. If the load is high enough, some queued jobs may be dropped. GitHub suggests scheduling your workflow at a different time of the hour, for example <code>17 3 * * *</code> instead of <code>0 3 * * *</code>.</p>
</li>
<li><p>Was a scheduled run created? Filter your runs by event with <code>gh run list --workflow your-workflow.yml --event schedule</code> (or the REST API: GET /repos/OWNER/REPO/actions/runs?event=schedule). A run that was created but failed still appears in this list, so this tells a run that was not created apart from one that started late or failed.</p>
</li>
<li><p>If manual runs are also blocked: are Actions minutes available? Private repositories get a quota of free minutes. If the account has no valid payment method on file, usage is blocked once that quota is used up, and budgets can also limit spending. Check the account's billing settings.</p>
</li>
<li><p>Enterprise Managed Users: scheduled workflows won't run if their last actor has been deprovisioned by the identity provider.</p>
</li>
</ol>
<p>If those checks pass: the cause may be on GitHub's end. Check GitHub's status page for Actions incidents, and keep in mind the documented note that scheduled runs can be delayed, or queued jobs dropped, under high load. In the meantime, if the workflow also has a workflow_dispatch trigger, you can start it by hand from the Actions tab.</p>
<p>Sources (checked October 21, 2026): GitHub Docs, "Events that trigger workflows" (schedule section); GitHub Docs, "Disabling and enabling a workflow"; GitHub CLI manual, "gh run list"; GitHub Docs, REST API "List workflow runs for a repository"; GitHub Docs, "GitHub Actions billing"; GitHub Status.</p>
<p>This post was drafted with AI tools and checked before it went out.</p>
<p>I make ShipKit, which gives away this guide and sells the paid <a href="https://8686702537545.gumroad.com/l/clhec?ref=hashnode-cron">GitHub Actions Starter Pack</a>.</p>
]]></content:encoded></item></channel></rss>