{"id":79582,"date":"2026-10-28T07:48:05","date_gmt":"2026-10-28T07:48:05","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=79582"},"modified":"2026-10-28T07:48:07","modified_gmt":"2026-10-28T07:48:07","slug":"what-is-github-actions-runner-controller","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/what-is-github-actions-runner-controller\/","title":{"rendered":"What Is GitHub Actions Runner Controller?"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">GitHub Actions is a CI\/CD platform that automates tasks such as building applications, running tests, creating Docker images, and deploying software.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every GitHub Actions job requires a <strong>runner<\/strong>, which is a machine or container responsible for executing the workflow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub provides two main runner models:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>GitHub-hosted runners:<\/strong> Virtual machines managed by GitHub.<\/li>\n\n\n\n<li><strong>Self-hosted runners:<\/strong> Compute infrastructure operated by your organization.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Actions Runner Controller (ARC) extends the self-hosted approach by running GitHub Actions runners on Kubernetes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of keeping dedicated virtual machines permanently online, ARC can dynamically create Kubernetes runner pods when workflow jobs arrive and remove them after execution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why use ARC on Amazon EKS?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Amazon Elastic Kubernetes Service (EKS) provides a managed Kubernetes control plane. ARC uses Kubernetes resources to orchestrate GitHub Actions runners on the compute resources attached to the EKS cluster.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This combination provides several benefits:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Automatic runner scaling:<\/strong> Runner pods scale with GitHub Actions demand.<\/li>\n\n\n\n<li><strong>Ephemeral execution:<\/strong> Each job can execute in a fresh runner environment.<\/li>\n\n\n\n<li><strong>Infrastructure control:<\/strong> Organizations choose CPU, memory, architecture, images, networking, and security controls.<\/li>\n\n\n\n<li><strong>AWS integration:<\/strong> Workflows can interact with services such as ECR, S3, EKS, and CloudWatch.<\/li>\n\n\n\n<li><strong>Workload isolation:<\/strong> Different runner scale sets can serve different projects or security boundaries.<\/li>\n\n\n\n<li><strong>Cost optimization:<\/strong> Idle runner pods can scale to zero, although the Kubernetes infrastructure and ARC control components still incur costs.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">ARC is especially useful for organizations with substantial CI\/CD workloads, private infrastructure dependencies, custom build environments, or strict control requirements.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, it introduces Kubernetes and AWS operations overhead. For small projects using standard build tooling, GitHub-hosted runners may remain simpler and more economical.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Understanding the ARC Architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The modern GitHub-supported ARC architecture uses <strong>Autoscaling Runner Scale Sets<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Its main components are:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Component<\/th><th>Responsibility<\/th><\/tr><tr><td>GitHub Actions service<\/td><td>Receives workflows, schedules jobs, and communicates with runner scale sets<\/td><\/tr><tr><td>ARC controller manager<\/td><td>Reconciles Kubernetes resources and manages runner lifecycle<\/td><\/tr><tr><td>AutoScalingRunnerSet<\/td><td>Represents a logical group of autoscaling GitHub runners<\/td><\/tr><tr><td>Runner scale-set listener<\/td><td>Maintains a connection with GitHub Actions and receives job availability notifications<\/td><\/tr><tr><td>EphemeralRunnerSet<\/td><td>Represents the desired group of ephemeral runners<\/td><\/tr><tr><td>EphemeralRunner<\/td><td>Manages the lifecycle of an individual runner<\/td><\/tr><tr><td>Runner pod<\/td><td>Executes a GitHub Actions job<\/td><\/tr><tr><td>EKS compute<\/td><td>Provides the EC2-backed Kubernetes nodes on which runner pods execute<\/td><\/tr><tr><td>External cache<\/td><td>Stores reusable dependencies or build results between ephemeral jobs<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Architecture diagram<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">flowchart TB\n    DEV&#91;<span class=\"hljs-string\">\"Developer pushes code\"<\/span>] --&gt; GH&#91;<span class=\"hljs-string\">\"GitHub Actions Service\"<\/span>]\n\n    subgraph EKS&#91;<span class=\"hljs-string\">\"Amazon EKS Cluster\"<\/span>]\n        subgraph CONTROL&#91;<span class=\"hljs-string\">\"ARC Control Components\"<\/span>]\n            CTRL&#91;<span class=\"hljs-string\">\"ARC Controller Manager\"<\/span>]\n            LISTENER&#91;<span class=\"hljs-string\">\"Runner Scale Set Listener\"<\/span>]\n        end\n\n        SCALE&#91;<span class=\"hljs-string\">\"AutoScalingRunnerSet\"<\/span>]\n        PODS&#91;<span class=\"hljs-string\">\"Ephemeral Runner Pods\"<\/span>]\n        NODES&#91;<span class=\"hljs-string\">\"EKS Worker Nodes\"<\/span>]\n\n        LISTENER --&gt; SCALE\n        SCALE --&gt; CTRL\n        CTRL --&gt; PODS\n        PODS --&gt; NODES\n    end\n\n    GH &lt;--&gt;|HTTPS job notifications| LISTENER\n    GH &lt;--&gt;|Job communication| PODS\n\n    PODS &lt;--&gt; GHC&#91;<span class=\"hljs-string\">\"GitHub Actions Cache\"<\/span>]\n    PODS &lt;--&gt; ECR&#91;<span class=\"hljs-string\">\"Amazon ECR Build Cache\"<\/span>]\n    PODS &lt;--&gt; S3&#91;<span class=\"hljs-string\">\"Amazon S3 Artifacts\"<\/span>]\n\n    style GH fill:<span class=\"hljs-comment\">#dbeafe,color:#172554<\/span>\n    style EKS fill:<span class=\"hljs-comment\">#f8fafc,color:#0f172a<\/span>\n    style CONTROL fill:<span class=\"hljs-comment\">#d1fae5,color:#064e3b<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The important architectural distinction is that <strong>ARC manages runner execution, while GitHub remains responsible for workflow orchestration and job scheduling<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The controller and listener do not execute application build steps. The ephemeral runner pods execute the steps.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does ARC need an application load balancer?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. A normal ARC deployment does not require an ALB or a publicly accessible inbound endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The controller, listener, and runners communicate with GitHub using outbound HTTPS connections.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You need suitable outbound connectivity to GitHub, container registries, package repositories, and any services your workflows depend on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. How ARC Executes a GitHub Actions Job<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Consider a developer pushing code to a repository configured with:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">jobs:\n  build:\n    runs-on: arc-linux<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The execution lifecycle is:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>The developer pushes code.<\/li>\n\n\n\n<li>GitHub creates a workflow run and queues the job.<\/li>\n\n\n\n<li>GitHub identifies the compatible runner scale set.<\/li>\n\n\n\n<li>The scale-set listener receives a job availability notification.<\/li>\n\n\n\n<li>ARC adjusts the desired runner capacity.<\/li>\n\n\n\n<li>Kubernetes creates an ephemeral runner pod.<\/li>\n\n\n\n<li>The runner registers with GitHub using just-in-time registration.<\/li>\n\n\n\n<li>GitHub assigns the job to the runner.<\/li>\n\n\n\n<li>The runner restores available caches, executes workflow steps, and publishes results.<\/li>\n\n\n\n<li>After the job finishes, the ephemeral runner terminates and ARC cleans up its Kubernetes resources.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Job execution sequence diagram<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">sequenceDiagram\n    autonumber\n    participant Dev <span class=\"hljs-keyword\">as<\/span> Developer\n    participant GH <span class=\"hljs-keyword\">as<\/span> GitHub Actions\n    participant Listener <span class=\"hljs-keyword\">as<\/span> ARC Listener\n    participant ARC <span class=\"hljs-keyword\">as<\/span> ARC Controller\n    participant EKS <span class=\"hljs-keyword\">as<\/span> Kubernetes\n    participant Cache <span class=\"hljs-keyword\">as<\/span> External Cache\n\n    Dev-&gt;&gt;GH: Push commit\n    GH-&gt;&gt;GH: Queue workflow job\n    GH--&gt;&gt;Listener: Job available\n    Listener-&gt;&gt;ARC: Request runner capacity\n    ARC-&gt;&gt;EKS: Create ephemeral runner pod\n    EKS--&gt;&gt;ARC: Runner pod ready\n    EKS-&gt;&gt;GH: Register runner\n    GH-&gt;&gt;EKS: Assign job\n    EKS-&gt;&gt;Cache: Restore cache\n    Cache--&gt;&gt;EKS: Cached dependencies\n    EKS-&gt;&gt;EKS: Build and test\n    EKS-&gt;&gt;Cache: Export updated cache\n    EKS--&gt;&gt;GH: Report results\n    EKS-&gt;&gt;GH: Complete runner session\n    ARC-&gt;&gt;EKS: Clean up runner pod<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This explains why a cache stored only on an ephemeral runner&#8217;s local filesystem is not suitable for reuse across multiple jobs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once the runner pod disappears, its temporary filesystem normally disappears with it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">External caches solve that problem.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. ARC Runner Execution Modes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ARC supports different execution configurations. Choosing the right mode is important for security, compatibility, and performance.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Mode<\/th><th>How it works<\/th><th>Suitable workloads<\/th><th>Key consideration<\/th><\/tr><tr><td>Native runner<\/td><td>Workflow steps run directly in the runner container<\/td><td>CLI, Python, Node.js, Terraform, tests<\/td><td>No Docker daemon provided automatically<\/td><\/tr><tr><td>Docker-in-Docker (<code>dind<\/code>)<\/td><td>Docker daemon runs alongside the runner<\/td><td>Docker builds, Docker actions, service containers<\/td><td>Requires privileged execution<\/td><\/tr><tr><td>Kubernetes mode<\/td><td>Container jobs execute in Kubernetes-managed pods<\/td><td>Containerized workflows<\/td><td>Requires workspace-sharing configuration<\/td><\/tr><tr><td>Kubernetes no-volume mode<\/td><td>Uses container lifecycle hooks for job filesystem transfer<\/td><td>Containerized workflows without shared PVCs<\/td><td>Requires compatibility testing<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Recommended approach:<\/strong> Start with native runners for ordinary CLI and dependency-based jobs. Add a separate Docker-capable runner scale set only when the workflow requires it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Docker-in-Docker is convenient but creates a significant security boundary concern. AWS recommends avoiding privileged Docker-in-Docker workloads on clusters shared with production applications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Kubernetes mode is not automatically a drop-in replacement for Docker mode. In particular, job container requirements and workspace behavior need to be tested.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 2 \u2014 Install ARC on Amazon EKS<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">5. Prerequisites<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before proceeding, you need:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An AWS account and an existing Amazon EKS cluster<\/li>\n\n\n\n<li><code>aws<\/code> CLI with EKS access<\/li>\n\n\n\n<li><code>kubectl<\/code><\/li>\n\n\n\n<li>Helm 3<\/li>\n\n\n\n<li>A GitHub organization or repository where you can configure self-hosted runners<\/li>\n\n\n\n<li>Permission to create or install a GitHub App<\/li>\n\n\n\n<li>Outbound HTTPS connectivity from Kubernetes pods<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The following examples use the Tokyo AWS region, <code>ap-northeast-1<\/code>. Substitute your own region and cluster name as appropriate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Check installed tools<\/h3>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">aws --version\nkubectl version --client\nhelm version<\/code><\/span><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Step 2: Connect kubectl to the EKS cluster<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> AWS_REGION=<span class=\"hljs-string\">\"ap-northeast-1\"<\/span>\n<span class=\"hljs-keyword\">export<\/span> EKS_CLUSTER_NAME=<span class=\"hljs-string\">\"arc-lab\"<\/span>\n\naws eks update-kubeconfig \\\n  --region <span class=\"hljs-string\">\"${AWS_REGION}\"<\/span> \\\n  --name <span class=\"hljs-string\">\"${EKS_CLUSTER_NAME}\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Confirm access:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl cluster-info\nkubectl <span class=\"hljs-keyword\">get<\/span> nodes -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> namespaces<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">If your cluster has EKS Auto Mode enabled, the managed compute provisioning system can respond to new runner pod requests. Otherwise, configure managed node groups or another supported node autoscaler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ARC automatically scales runner pods, but it does not independently provision EC2 nodes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Optional: Create a dedicated EKS test cluster<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you do not already have an EKS cluster, <code>eksctl<\/code> can create one using this example file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>eks-cluster.yaml<\/code><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"HTTP\" data-shcb-language-slug=\"http\"><span><code class=\"hljs language-http\"><span class=\"hljs-attribute\">apiVersion<\/span>: eksctl.io\/v1alpha5\n<span class=\"hljs-attribute\">kind<\/span>: ClusterConfig\n\nmetadata:\n  name: arc-lab\n  region: ap-northeast-1\n\nmanagedNodeGroups:\n  - name: arc-workers\n    instanceType: m6i.large\n    desiredCapacity: 2\n    minSize: 1\n    maxSize: 5<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTTP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">http<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Create the cluster:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">eksctl<\/span> <span class=\"hljs-selector-tag\">create<\/span> <span class=\"hljs-selector-tag\">cluster<\/span> <span class=\"hljs-selector-tag\">-f<\/span> <span class=\"hljs-selector-tag\">eks-cluster<\/span><span class=\"hljs-selector-class\">.yaml<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This creates billable AWS infrastructure. Use it only in an authorized test account and review your expected EKS, EC2, EBS, and networking costs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For production, a dedicated CI\/CD cluster or a strongly isolated runner compute environment is preferable.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Configure GitHub Authentication<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ARC supports GitHub App authentication and personal access token authentication.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For repository-level and organization-level scale sets, <strong>GitHub App authentication is recommended<\/strong> because it avoids tying infrastructure authentication to an individual&#8217;s personal access token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Enterprise-level scale sets have different authentication requirements and currently require a supported classic PAT configuration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Create a GitHub App<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In your GitHub organization, open:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Settings \u2192 Developer settings \u2192 GitHub Apps \u2192 New GitHub App<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Configure the App using the permissions specified by GitHub&#8217;s ARC authentication documentation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For repository-scoped runner registration:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Repository Administration: Read and write<\/li>\n\n\n\n<li>Repository Metadata: Read-only<\/li>\n\n\n\n<li>Organization Self-hosted runners: Read and write<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For organization-scoped runner registration, the repository Administration permission is not required.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Install the App into the relevant organization.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Collect the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>GitHub App ID<\/li>\n\n\n\n<li>GitHub App installation ID<\/li>\n\n\n\n<li>Downloaded private key in PEM format<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Create Kubernetes namespaces<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Separate the controller from job-executing runner pods.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">kubectl create <span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">arc<\/span>-<span class=\"hljs-title\">systems<\/span>\n<span class=\"hljs-title\">kubectl<\/span> <span class=\"hljs-title\">create<\/span> <span class=\"hljs-title\">namespace<\/span> <span class=\"hljs-title\">arc<\/span>-<span class=\"hljs-title\">runners<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Step 3: Create the authentication secret<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Set the environment variables using the values obtained from the GitHub App.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> GITHUB_APP_ID=<span class=\"hljs-string\">\"YOUR_APP_ID\"<\/span>\n<span class=\"hljs-keyword\">export<\/span> GITHUB_APP_INSTALLATION_ID=<span class=\"hljs-string\">\"YOUR_INSTALLATION_ID\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Create the Kubernetes secret:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl create secret generic arc-github-app \\\n  --namespace arc-runners \\\n  --<span class=\"hljs-keyword\">from<\/span>-literal=github_app_id=<span class=\"hljs-string\">\"${GITHUB_APP_ID}\"<\/span> \\\n  --<span class=\"hljs-keyword\">from<\/span>-literal=github_app_installation_id=<span class=\"hljs-string\">\"${GITHUB_APP_INSTALLATION_ID}\"<\/span> \\\n  --<span class=\"hljs-keyword\">from<\/span>-file=github_app_private_key=.\/github-app-private-key.pem<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Verify that the secret exists without printing its contents:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> secrets -n arc-runners<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">For production, use Kubernetes secret encryption, strict RBAC, and an approved secret-management integration. Never commit the GitHub App private key to a repository or embed it in Helm values.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Install the ARC Controller<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub distributes the modern ARC charts using the Open Container Initiative (OCI) format.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The verified ARC chart release at the time of this guide is <code>0.15.0<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set the chart version:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> ARC_VERSION=<span class=\"hljs-string\">\"0.15.0\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Install the controller:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">helm upgrade --install arc \\\n  --namespace arc-systems \\\n  --version <span class=\"hljs-string\">\"${ARC_VERSION}\"<\/span> \\\n  <span class=\"hljs-attr\">oci<\/span>:<span class=\"hljs-comment\">\/\/ghcr.io\/actions\/actions-runner-controller-charts\/gha-runner-scale-set-controller \\<\/span>\n  --wait<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Verify:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">helm list -n arc-systems\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n arc-systems\nkubectl <span class=\"hljs-keyword\">get<\/span> crds | grep actions.github.com<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">A successful installation should produce an ARC controller manager pod in the <code>Running<\/code> state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not use legacy instructions that install the old <code>actions-runner-controller<\/code> chart and <code>RunnerDeployment<\/code> resources for a new deployment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. Create an Autoscaling Runner Scale Set<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create a file named <code>values-arc-linux.yaml<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">githubConfigUrl: <span class=\"hljs-string\">\"https:\/\/github.com\/YOUR_ORG\"<\/span>\n\n<span class=\"hljs-attr\">githubConfigSecret<\/span>: <span class=\"hljs-string\">\"arc-github-app\"<\/span>\n\n<span class=\"hljs-attr\">runnerScaleSetName<\/span>: <span class=\"hljs-string\">\"arc-linux\"<\/span>\n\n<span class=\"hljs-attr\">minRunners<\/span>: <span class=\"hljs-number\">0<\/span>\n<span class=\"hljs-attr\">maxRunners<\/span>: <span class=\"hljs-number\">10<\/span>\n\n<span class=\"hljs-attr\">template<\/span>:\n  spec:\n    containers:\n      - name: runner\n        <span class=\"hljs-attr\">image<\/span>: ghcr.io\/actions\/actions-runner:latest\n        <span class=\"hljs-attr\">command<\/span>: &#91;<span class=\"hljs-string\">\"\/home\/runner\/run.sh\"<\/span>]\n        <span class=\"hljs-attr\">resources<\/span>:\n          requests:\n            cpu: <span class=\"hljs-string\">\"500m\"<\/span>\n            <span class=\"hljs-attr\">memory<\/span>: <span class=\"hljs-string\">\"1Gi\"<\/span>\n          <span class=\"hljs-attr\">limits<\/span>:\n            cpu: <span class=\"hljs-string\">\"2\"<\/span>\n            <span class=\"hljs-attr\">memory<\/span>: <span class=\"hljs-string\">\"4Gi\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Replace <code>YOUR_ORG<\/code> with the actual GitHub organization name. For repository-level registration, use the full repository URL instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Configuration explained<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Parameter<\/th><th>Explanation<\/th><\/tr><tr><td><code>githubConfigUrl<\/code><\/td><td>GitHub repository, organization, or enterprise where the scale set belongs<\/td><\/tr><tr><td><code>githubConfigSecret<\/code><\/td><td>Kubernetes secret containing ARC GitHub credentials<\/td><\/tr><tr><td><code>runnerScaleSetName<\/code><\/td><td>Logical runner name used in <code>runs-on<\/code><\/td><\/tr><tr><td><code>minRunners<\/code><\/td><td>Minimum number of available idle runners<\/td><\/tr><tr><td><code>maxRunners<\/code><\/td><td>Maximum runner capacity<\/td><\/tr><tr><td><code>template<\/code><\/td><td>Kubernetes runner pod definition<\/td><\/tr><tr><td><code>resources.requests<\/code><\/td><td>CPU and memory requested for Kubernetes scheduling<\/td><\/tr><tr><td><code>resources.limits<\/code><\/td><td>Maximum CPU and memory allowed for the runner<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For this example:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"HTTP\" data-shcb-language-slug=\"http\"><span><code class=\"hljs language-http\"><span class=\"hljs-attribute\">minRunners<\/span>: 0\n<span class=\"hljs-attribute\">maxRunners<\/span>: 10<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTTP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">http<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The runner set can scale down to zero idle runners and scale up to ten runners as demand increases.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Important:<\/strong> Setting both <code>minRunners<\/code> and <code>maxRunners<\/code> to zero is different. In current ARC configuration, that combination can be used to drain the queue by preventing new runner pods from being created.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Install the scale set<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">helm upgrade --install arc-linux \\\n  --namespace arc-runners \\\n  --version <span class=\"hljs-string\">\"${ARC_VERSION}\"<\/span> \\\n  -f values-arc-linux.yaml \\\n  <span class=\"hljs-attr\">oci<\/span>:<span class=\"hljs-comment\">\/\/ghcr.io\/actions\/actions-runner-controller-charts\/gha-runner-scale-set \\<\/span>\n  --wait<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Verify:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">helm list -A\n\nkubectl <span class=\"hljs-keyword\">get<\/span> autoscalingrunnersets -A\n\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n arc-runners<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The ARC listener should be available even when the configuration has zero idle runner pods.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">9. Run the First GitHub Actions Workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Inside your GitHub repository, create:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>.github\/workflows\/arc-test.yml<\/code><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">name: ARC EKS Runner Test\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    runs-on: arc-linux\n\n    steps:\n      - name: Runner information\n        run: |\n          <span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"Running on ARC\"<\/span>\n          <span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"Runner: $RUNNER_NAME\"<\/span>\n          <span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"Operating system: $RUNNER_OS\"<\/span>\n          uname -a\n          nproc\n          free -h\n          df -h\n\n      - name: Validate job execution\n        run: |\n          <span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"ARC job completed successfully\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Commit the workflow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Open the repository&#8217;s <strong>Actions<\/strong> tab, select the workflow, and run it manually.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While the workflow executes, monitor the Kubernetes namespace:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> pods -n arc-runners -w<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">You should observe a runner pod being created, executing the job, and eventually disappearing after completion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the workflow remains queued, inspect the runner scale-set name, listener logs, GitHub App permissions, and Kubernetes capacity.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 3 \u2014 GitHub Actions Caching Explained<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">10. Why Caching Is Important for Ephemeral ARC Runners<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Consider a Node.js project that installs hundreds of packages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Without caching, a workflow might repeatedly download many of the same packages, even when its dependency lockfile has not changed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same issue affects:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>npm and pnpm packages<\/li>\n\n\n\n<li>Python pip dependencies<\/li>\n\n\n\n<li>Maven repositories<\/li>\n\n\n\n<li>Gradle dependencies and transforms<\/li>\n\n\n\n<li>Go modules<\/li>\n\n\n\n<li>Docker image layers<\/li>\n\n\n\n<li>Compilers, toolchains, and reusable generated assets<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ephemeral ARC runners generally do not retain local files between separate workflow jobs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A remote cache allows a new runner to reuse content produced during an earlier execution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GitHub Actions cache lifecycle<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">flowchart TD\n    START&#91;<span class=\"hljs-string\">\"New ARC runner starts\"<\/span>] --&gt; KEY&#91;<span class=\"hljs-string\">\"Calculate cache key\"<\/span>]\n    KEY --&gt; LOOKUP{<span class=\"hljs-string\">\"Matching cache exists?\"<\/span>}\n\n    LOOKUP --&gt;|Yes| RESTORE&#91;<span class=\"hljs-string\">\"Download and extract cache\"<\/span>]\n    LOOKUP --&gt;|No| COLD&#91;<span class=\"hljs-string\">\"Download dependencies normally\"<\/span>]\n\n    RESTORE --&gt; BUILD&#91;<span class=\"hljs-string\">\"Run build and tests\"<\/span>]\n    COLD --&gt; BUILD\n\n    BUILD --&gt; RESULT{<span class=\"hljs-string\">\"Job successful?\"<\/span>}\n    RESULT --&gt;|No| END&#91;<span class=\"hljs-string\">\"Finish without normal cache save\"<\/span>]\n    RESULT --&gt;|Yes| SAVE{<span class=\"hljs-string\">\"New cache key and write allowed?\"<\/span>}\n    SAVE --&gt;|Yes| UPLOAD&#91;<span class=\"hljs-string\">\"Upload cache archive\"<\/span>]\n    SAVE --&gt;|No| END\n    UPLOAD --&gt; END<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The first run usually experiences a cold cache. Later runs may restore content through exact or compatible cache-key matches.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub cache entries are immutable. Once a cache exists under a given key and cache version, the normal pattern is to generate a new key when the reusable content changes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">11. GitHub Actions Cache Options Compared<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There is no single cache mechanism that is optimal for all workloads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A useful distinction is between dependency caches, Docker build caches, object storage, and shared filesystems.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Caching solution<\/th><th>Best suited for<\/th><th>Persistent across ARC jobs?<\/th><th>Complexity<\/th><th>Assessment<\/th><\/tr><tr><td><code>actions\/cache<\/code><\/td><td>General dependencies and reusable directories<\/td><td>Yes<\/td><td>Low<\/td><td>Recommended general-purpose choice<\/td><\/tr><tr><td><code>setup-*<\/code> action caching<\/td><td>npm, pip, Maven, Go, and similar tools<\/td><td>Yes<\/td><td>Very low<\/td><td>Best default for supported package managers<\/td><\/tr><tr><td>BuildKit <code>type=gha<\/code><\/td><td>Docker image build layers<\/td><td>Yes<\/td><td>Low<\/td><td>Good for smaller Docker caches<\/td><\/tr><tr><td>BuildKit <code>type=registry<\/code> with ECR<\/td><td>Docker image build layers<\/td><td>Yes<\/td><td>Medium<\/td><td>Strong choice for AWS-centric builds<\/td><\/tr><tr><td>Amazon S3 with custom scripts<\/td><td>Large archives and reusable test assets<\/td><td>Yes<\/td><td>Medium<\/td><td>Useful for specialized data<\/td><\/tr><tr><td>Amazon EFS<\/td><td>Shared filesystem workloads<\/td><td>Yes<\/td><td>Medium\u2013High<\/td><td>Use only with carefully designed concurrency<\/td><\/tr><tr><td>Local disk or <code>emptyDir<\/code><\/td><td>Temporary workspace within one runner pod<\/td><td>Generally no<\/td><td>Low<\/td><td>Fast but not persistent<\/td><\/tr><tr><td>ElastiCache Redis<\/td><td>Small key\/value objects or custom application cache data<\/td><td>Yes<\/td><td>High<\/td><td>Not a typical CI dependency cache<\/td><\/tr><tr><td>Amazon RDS PostgreSQL<\/td><td>Relational application and test data<\/td><td>Yes<\/td><td>High<\/td><td>Not recommended as a build-cache backend<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">A. Native GitHub Actions Cache<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the simplest general-purpose solution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Advantages include native GitHub integration, support for self-hosted runners, branch-aware cache access, lockfile-based cache keys, and relatively little operational effort.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Limitations include remote upload\/download time, cache eviction, storage quotas, and network transfer through your EKS networking configuration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Docker BuildKit GitHub Cache<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The BuildKit <code>gha<\/code> backend stores Docker build cache data using the GitHub Actions cache service.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is particularly useful when Docker builds remain within the cache size and usage limits provided by GitHub.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, Docker still identifies this backend as experimental and documents API throttling considerations.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">C. Amazon ECR Build Cache<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">BuildKit&#8217;s registry cache backend can store reusable Docker build layers in ECR.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is attractive when the workload already builds and pushes images to ECR, requires larger caches, or benefits from AWS-regional storage and controlled access.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It also provides independent lifecycle management, but consumes registry storage and requires suitable IAM permissions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">D. Amazon S3<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">S3 is appropriate when your workload needs to store reusable archives or datasets that are not naturally handled by the GitHub cache service.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Examples include large test fixtures, downloaded reference data, generated assets, and custom dependency archives.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Implement your own object naming, integrity checking, retention, and concurrency control.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A particularly important distinction: Docker documents its direct BuildKit S3 cache exporter as unreleased. Do not assume that configuring <code>cache-to: type=s3<\/code> is a supported production solution in standard Buildx distributions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Amazon EFS<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">EFS can share a filesystem across multiple pods.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This may be useful for a centrally maintained read-only dependency mirror or specialized workloads that require shared files.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, sharing mutable build directories between concurrent jobs can create locking problems, corruption risks, and unpredictable performance.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">EFS is not automatically faster than downloading a compressed cache archive from GitHub.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">F. RDS PostgreSQL<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RDS PostgreSQL is designed for relational data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It does not provide a native backend for <code>actions\/cache<\/code>, nor is it a standard BuildKit cache exporter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Implementing a file cache using PostgreSQL would require custom application logic, binary storage management, eviction policies, concurrent-access handling, and database administration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Unless your workflow specifically needs relational data, PostgreSQL introduces unnecessary complexity and expense.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Conclusion:<\/strong> Do not deploy RDS PostgreSQL just to cache ARC build artifacts.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 4 \u2014 Implement GitHub Actions Caching<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">12. Node.js and npm Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For most Node.js projects, use the built-in cache support in <code>actions\/setup-node<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>.github\/workflows\/node-ci.yml<\/code><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">name: Node.js CI\n\n<span class=\"hljs-attr\">on<\/span>:\n  push:\n    branches: &#91;main]\n  <span class=\"hljs-attr\">workflow_dispatch<\/span>:\n\npermissions:\n  contents: read\n\n<span class=\"hljs-attr\">jobs<\/span>:\n  build:\n    runs-on: arc-linux\n\n    <span class=\"hljs-attr\">steps<\/span>:\n      - uses: actions\/checkout@v7\n        <span class=\"hljs-attr\">with<\/span>:\n          persist-credentials: <span class=\"hljs-literal\">false<\/span>\n\n      - name: Setup Node.js\n        <span class=\"hljs-attr\">uses<\/span>: actions\/setup-node@v7\n        <span class=\"hljs-attr\">with<\/span>:\n          node-version: <span class=\"hljs-string\">\"24\"<\/span>\n          <span class=\"hljs-attr\">cache<\/span>: npm\n          cache-dependency-path: package-lock.json\n\n      - name: Install dependencies\n        <span class=\"hljs-attr\">run<\/span>: npm ci\n\n      - name: Run tests\n        <span class=\"hljs-attr\">run<\/span>: npm test\n\n      - name: Build application\n        <span class=\"hljs-attr\">run<\/span>: npm run build<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">What does this cache?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The setup action caches npm&#8217;s package download cache, not the complete <code>node_modules<\/code> directory.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow still executes <code>npm ci<\/code>, but npm can reuse previously downloaded package data when available.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This preserves a clean dependency installation while reducing repeated network downloads.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why use the package-lock.json file?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The lockfile identifies the dependency versions required by the project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When it changes, the cache key can change, allowing an updated cache to be generated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For monorepos, use the correct <code>cache-dependency-path<\/code> to avoid unrelated applications sharing inappropriate cache keys.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">13. Python pip Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For Python projects, use <code>actions\/setup-python<\/code> with pip caching.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"HTTP\" data-shcb-language-slug=\"http\"><span><code class=\"hljs language-http\"><span class=\"hljs-attribute\">name<\/span>: Python CI\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    runs-on: arc-linux\n\n    steps:\n      - uses: actions\/checkout@v7\n\n      - uses: actions\/setup-python@v7\n        with:\n          python-version: \"3.13\"\n          cache: pip\n          cache-dependency-path: requirements.txt\n\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n\n      - name: Execute tests\n        run: pytest<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTTP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">http<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The cache reduces repeated package downloads. It does not guarantee that every dependency installation or wheel build will be eliminated.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">14. Maven Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For Java applications using Maven:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"HTTP\" data-shcb-language-slug=\"http\"><span><code class=\"hljs language-http\"><span class=\"hljs-attribute\">name<\/span>: Maven CI\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    runs-on: arc-linux\n\n    steps:\n      - uses: actions\/checkout@v7\n\n      - uses: actions\/setup-java@v6\n        with:\n          distribution: temurin\n          java-version: \"21\"\n          cache: maven\n\n      - name: Maven build\n        run: mvn --batch-mode verify<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTTP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">http<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The action manages dependency caching and related Java setup behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For applications with complicated Maven repositories, consider a centralized artifact repository such as Nexus or Artifactory in addition to workflow-level caching.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is a different architecture from replacing GitHub cache with PostgreSQL.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">15. Gradle and Android Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Gradle deserves special attention because Android applications often have substantial dependency and build-transform costs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The official Gradle GitHub Action supports more sophisticated caching than manually archiving arbitrary directories.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"HTTP\" data-shcb-language-slug=\"http\"><span><code class=\"hljs language-http\"><span class=\"hljs-attribute\">name<\/span>: Gradle CI\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    runs-on: arc-linux\n\n    steps:\n      - uses: actions\/checkout@v7\n\n      - uses: actions\/setup-java@v6\n        with:\n          distribution: temurin\n          java-version: \"21\"\n\n      - uses: gradle\/actions\/setup-gradle@v6\n        with:\n          cache-provider: basic\n\n      - name: Run Gradle build\n        run: .\/gradlew build --no-daemon<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTTP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">http<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">A newer Gradle caching consideration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The official Gradle action now distinguishes between an enhanced caching provider and a basic caching provider.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The enhanced option is the default in its current documentation and uses an additional proprietary caching component. The basic option uses the standard GitHub Actions cache mechanism and is entirely open source.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you want a straightforward, independently understandable caching configuration, explicitly select:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"HTTP\" data-shcb-language-slug=\"http\"><span><code class=\"hljs language-http\"><span class=\"hljs-attribute\">cache-provider<\/span>: basic<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTTP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">http<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">If your organization accepts the enhanced provider&#8217;s licensing, data-handling, and service terms, benchmark it separately.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Avoid caching the Gradle User Home simultaneously through <code>setup-gradle<\/code>, <code>setup-java cache: gradle<\/code>, and a separate manual <code>actions\/cache<\/code> step. Overlapping mechanisms can interfere with each other.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For Android, also account for Android SDK installation time, toolchain downloads, emulator startup, and build output reuse. An optimized runner image with commonly needed tools preinstalled may help more than dependency caching alone.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">16. Custom Cache Keys with actions\/cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When a setup action does not cover your use case, configure <code>actions\/cache<\/code> directly.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">- name: Restore custom cache\n  <span class=\"hljs-attr\">id<\/span>: custom-cache\n  <span class=\"hljs-attr\">uses<\/span>: actions\/cache@v6\n  <span class=\"hljs-attr\">with<\/span>:\n    path: ~<span class=\"hljs-regexp\">\/.cache\/my<\/span>tool\n    <span class=\"hljs-attr\">key<\/span>: ${{ runner.os }}-${{ runner.arch }}-mytool-${{ hashFiles(<span class=\"hljs-string\">'tools.lock'<\/span>) }}\n    restore-keys: |\n      ${{ runner.os }}-${{ runner.arch }}-mytool-\n\n- name: Generate reusable data\n  <span class=\"hljs-attr\">if<\/span>: steps.custom-cache.outputs.cache-hit != <span class=\"hljs-string\">'true'<\/span>\n  <span class=\"hljs-attr\">run<\/span>: .\/scripts\/generate-cache.sh<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Use a stable, specific cache key. Include relevant dependency versions, operating system, architecture, or configuration hashes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not cache secrets, cloud credentials, access tokens, signing keys, or other sensitive data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For trusted and untrusted workflows, use GitHub&#8217;s cache permission model appropriately. Current GitHub Actions supports <code>cache-mode<\/code> values such as <code>read<\/code>, <code>write<\/code>, <code>write-only<\/code>, and <code>none<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 5 \u2014 Docker Builds and Amazon ECR Cache<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">17. Create a Docker-Capable ARC Runner Scale Set<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Native ARC runner pods do not automatically provide an operational Docker daemon.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For Docker-dependent builds, create a separate runner scale set with Docker-in-Docker mode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First create the namespace:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">kubectl create <span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">arc<\/span>-<span class=\"hljs-title\">docker<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Create the GitHub App secret again in this namespace because Kubernetes secrets are namespace-scoped.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl create secret generic arc-github-app \\\n  --namespace arc-docker \\\n  --<span class=\"hljs-keyword\">from<\/span>-literal=github_app_id=<span class=\"hljs-string\">\"${GITHUB_APP_ID}\"<\/span> \\\n  --<span class=\"hljs-keyword\">from<\/span>-literal=github_app_installation_id=<span class=\"hljs-string\">\"${GITHUB_APP_INSTALLATION_ID}\"<\/span> \\\n  --<span class=\"hljs-keyword\">from<\/span>-file=github_app_private_key=.\/github-app-private-key.pem<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Create <code>values-arc-docker.yaml<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">githubConfigUrl: <span class=\"hljs-string\">\"https:\/\/github.com\/YOUR_ORG\"<\/span>\n\n<span class=\"hljs-attr\">githubConfigSecret<\/span>: <span class=\"hljs-string\">\"arc-github-app\"<\/span>\n\n<span class=\"hljs-attr\">runnerScaleSetName<\/span>: <span class=\"hljs-string\">\"arc-docker\"<\/span>\n\n<span class=\"hljs-attr\">minRunners<\/span>: <span class=\"hljs-number\">0<\/span>\n<span class=\"hljs-attr\">maxRunners<\/span>: <span class=\"hljs-number\">5<\/span>\n\n<span class=\"hljs-attr\">containerMode<\/span>:\n  type: <span class=\"hljs-string\">\"dind\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Install it:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">helm upgrade --install arc-docker \\\n  --namespace arc-docker \\\n  --version <span class=\"hljs-string\">\"${ARC_VERSION}\"<\/span> \\\n  -f values-arc-docker.yaml \\\n  <span class=\"hljs-attr\">oci<\/span>:<span class=\"hljs-comment\">\/\/ghcr.io\/actions\/actions-runner-controller-charts\/gha-runner-scale-set \\<\/span>\n  --wait<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This mode uses privileged Docker execution. Deploy it only on approved, appropriately isolated compute.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A separate namespace is useful, but namespace separation alone does not provide a sufficient security boundary for privileged workloads. Prefer dedicated CI worker nodes or a dedicated EKS cluster, combined with appropriate network controls.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For organizations prioritizing unprivileged container execution, investigate Kubernetes mode, rootless BuildKit, or a separate managed build service such as AWS CodeBuild.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">18. Configure ECR Repositories<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following example assumes your AWS CLI is authenticated to the correct account.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> AWS_REGION=<span class=\"hljs-string\">\"ap-northeast-1\"<\/span>\n<span class=\"hljs-keyword\">export<\/span> APP_REPOSITORY=<span class=\"hljs-string\">\"arc-demo-app\"<\/span>\n<span class=\"hljs-keyword\">export<\/span> CACHE_REPOSITORY=<span class=\"hljs-string\">\"arc-demo-buildcache\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Create the repositories if they do not already exist:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">aws ecr create-repository \\\n  --repository-name <span class=\"hljs-string\">\"${APP_REPOSITORY}\"<\/span> \\\n  --region <span class=\"hljs-string\">\"${AWS_REGION}\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">aws ecr create-repository \\\n  --repository-name <span class=\"hljs-string\">\"${CACHE_REPOSITORY}\"<\/span> \\\n  --region <span class=\"hljs-string\">\"${AWS_REGION}\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The first repository stores final application images.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The second repository stores BuildKit&#8217;s reusable build-cache manifests and layers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create an AWS IAM role for GitHub Actions with permission to push and pull the required ECR images. Configure a restricted GitHub OIDC trust relationship for the intended repository and branch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not copy an old OIDC trust policy without checking the current <code>sub<\/code> claim format. GitHub&#8217;s OIDC claim format can differ based on repository creation date, organization configuration, and whether an environment is used.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set these GitHub repository or organization variables:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">AWS_ROLE_ARN\nECR_REPOSITORY\nECR_CACHE_REPOSITORY<\/code><\/span><\/pre>\n\n\n<h2 class=\"wp-block-heading\">19. Build Docker Images Using ECR Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>.github\/workflows\/docker-ecr.yml<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">name: Docker Build <span class=\"hljs-keyword\">with<\/span> ECR Cache\n\n<span class=\"hljs-attr\">on<\/span>:\n  push:\n    branches: &#91;main]\n  <span class=\"hljs-attr\">workflow_dispatch<\/span>:\n\npermissions:\n  contents: read\n  id-token: write\n\n<span class=\"hljs-attr\">jobs<\/span>:\n  docker:\n    runs-on: arc-docker\n\n    <span class=\"hljs-attr\">steps<\/span>:\n      - name: Checkout\n        <span class=\"hljs-attr\">uses<\/span>: actions\/checkout@v7\n        <span class=\"hljs-attr\">with<\/span>:\n          persist-credentials: <span class=\"hljs-literal\">false<\/span>\n\n      - name: Configure AWS credentials\n        <span class=\"hljs-attr\">uses<\/span>: aws-actions\/configure-aws-credentials@v6\n        <span class=\"hljs-attr\">with<\/span>:\n          role-to-assume: ${{ vars.AWS_ROLE_ARN }}\n          aws-region: ap-northeast<span class=\"hljs-number\">-1<\/span>\n\n      - name: Login to Amazon ECR\n        <span class=\"hljs-attr\">id<\/span>: ecr-login\n        <span class=\"hljs-attr\">uses<\/span>: aws-actions\/amazon-ecr-login@v2\n\n      - name: Setup Docker Buildx\n        <span class=\"hljs-attr\">uses<\/span>: docker\/setup-buildx-action@v4\n\n      - name: Build and push <span class=\"hljs-keyword\">with<\/span> registry cache\n        <span class=\"hljs-attr\">uses<\/span>: docker\/build-push-action@v7\n        <span class=\"hljs-attr\">with<\/span>:\n          context: .\n          push: <span class=\"hljs-literal\">true<\/span>\n          <span class=\"hljs-attr\">tags<\/span>: |\n            ${{ steps.ecr-login.outputs.registry }}\/${{ vars.ECR_REPOSITORY }}:${{ github.sha }}\n          cache-<span class=\"hljs-keyword\">from<\/span>: |\n            type=registry,ref=${{ steps.ecr-login.outputs.registry }}\/${{ vars.ECR_CACHE_REPOSITORY }}:main\n          cache-to: |\n            type=registry,ref=${{ steps.ecr-login.outputs.registry }}\/${{ vars.ECR_CACHE_REPOSITORY }}:main,mode=max,oci-mediatypes=<span class=\"hljs-literal\">true<\/span>,image-manifest=<span class=\"hljs-literal\">true<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">How this works<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The initial Docker build may have no usable external cache.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After a successful build, BuildKit exports reusable layers to the cache repository in ECR.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Subsequent builds can import those layers. Unchanged Dockerfile steps may then avoid expensive rebuilding.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>mode=max<\/code> setting allows caching of intermediate build layers, which can be particularly valuable for multi-stage Dockerfiles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The OCI manifest options provide an explicit ECR-compatible cache representation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A good Dockerfile remains essential. Stable dependency-installation steps should appear before frequently changing application source files so that source changes do not unnecessarily invalidate expensive dependency layers.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Concurrency warning<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The example uses the same <code>main<\/code> cache tag for builds of the default branch. For high-concurrency pipelines, manage cache writers carefully to avoid builds overwriting one another&#8217;s registry cache references.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One strategy is to serialize cache exports on the main branch. Another is to use separate branch or version-scoped cache references with controlled lifecycle management.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">20. Alternative: Docker BuildKit GitHub Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For simpler Docker projects, use BuildKit&#8217;s GitHub Actions cache backend.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-35\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">- name: Setup Docker Buildx\n  <span class=\"hljs-attr\">uses<\/span>: docker\/setup-buildx-action@v4\n\n- name: Docker build <span class=\"hljs-keyword\">with<\/span> GitHub cache\n  <span class=\"hljs-attr\">uses<\/span>: docker\/build-push-action@v7\n  <span class=\"hljs-attr\">with<\/span>:\n    context: .\n    push: <span class=\"hljs-literal\">false<\/span>\n    cache-<span class=\"hljs-keyword\">from<\/span>: type=gha,scope=myapp\n    cache-to: type=gha,scope=myapp,mode=max<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The <code>scope<\/code> parameter is important when multiple Docker images share one GitHub repository.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Without separate scopes, distinct image builds can overwrite each other&#8217;s BuildKit cache entries.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This configuration also requires a compatible Buildx and BuildKit release and suitable GitHub cache write permissions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For lower-trust workflows that have read-only cache access, use <code>cache-from<\/code> without attempting an export.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">ECR versus GitHub Cache for Docker<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Factor<\/th><th>GitHub <code>type=gha<\/code><\/th><th>ECR <code>type=registry<\/code><\/th><\/tr><tr><td>Initial configuration<\/td><td>Simpler<\/td><td>Requires ECR and IAM<\/td><\/tr><tr><td>GitHub integration<\/td><td>Native workflow cache service<\/td><td>AWS registry integration<\/td><\/tr><tr><td>Larger caches<\/td><td>Subject to GitHub cache quotas<\/td><td>Subject to ECR storage and registry limits<\/td><\/tr><tr><td>Cache visibility<\/td><td>GitHub Actions cache management<\/td><td>ECR repositories and image references<\/td><\/tr><tr><td>Sharing across repositories<\/td><td>Limited by GitHub cache scope<\/td><td>Possible with explicit AWS access<\/td><\/tr><tr><td>Eviction<\/td><td>GitHub cache policies<\/td><td>ECR lifecycle policies<\/td><\/tr><tr><td>AWS VPC connectivity<\/td><td>Requires access to GitHub cache services<\/td><td>Can use ECR and S3 VPC endpoints<\/td><\/tr><tr><td>Best fit<\/td><td>Smaller or moderate Docker build caches<\/td><td>AWS-centric, substantial Docker workloads<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Neither backend is universally faster.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub cache may perform very well for small caches with high reuse. ECR may be advantageous when large layers are repeatedly transferred within AWS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The actual result depends on where your runners execute, layer sizes, hit rates, and data transfer costs.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 6 \u2014 Performance, Costs and Operations<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">21. How Much Time Can Caching Actually Save?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Caching is useful when the time spent restoring reusable data is less than the time needed to recreate it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A practical formula is:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Net time saved = Time to recreate content \u2212 Cache restore time \u2212 Additional cache overhead<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Consider an illustrative pipeline:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Step<\/th><th>Without cache<\/th><th>With warm cache<\/th><\/tr><tr><td>Runner startup and checkout<\/td><td>2 minutes<\/td><td>2 minutes<\/td><\/tr><tr><td>Dependency installation<\/td><td>4 minutes<\/td><td>1 minute<\/td><\/tr><tr><td>Application build and tests<\/td><td>6 minutes<\/td><td>6 minutes<\/td><\/tr><tr><td>Additional cache handling<\/td><td>0 minutes<\/td><td>30 seconds<\/td><\/tr><tr><td><strong>Total<\/strong><\/td><td><strong>12 minutes<\/strong><\/td><td><strong>9.5 minutes<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This would represent approximately a <strong>21% reduction in overall job duration<\/strong>, even though dependency installation itself becomes substantially faster.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These figures are an arithmetic illustration, not measured ARC benchmarks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At 1,000 equivalent jobs per month, saving 2.5 minutes per job would avoid about 2,500 runner-minutes, or 41.7 runner-hours.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Whether that translates into lower EC2 cost depends on your node sizes, concurrency, scheduling, idle capacity, and node autoscaling behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, if EKS nodes remain running continuously regardless of job duration, faster jobs improve throughput without necessarily reducing the EC2 bill.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">When caching can be slower<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Caching may make a workflow slower when:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Cache archives are larger than the original package downloads.<\/li>\n\n\n\n<li>Upload and extraction costs exceed the work avoided.<\/li>\n\n\n\n<li>Cache keys change too frequently.<\/li>\n\n\n\n<li>Large caches are evicted before reuse.<\/li>\n\n\n\n<li>Network throughput is poor.<\/li>\n\n\n\n<li>The workload is dominated by compilation or tests rather than reusable downloads.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Measure before assuming.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">22. Current GitHub Cache Storage Pricing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub&#8217;s current documentation describes the following cache-storage model:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Default cache allowance: 10 GB per repository.<\/li>\n\n\n\n<li>The configured repository limit can be increased by eligible administrators.<\/li>\n\n\n\n<li>Additional cache storage above the included allowance is billed.<\/li>\n\n\n\n<li>Published cache-storage price: $0.07 per GB-month.<\/li>\n\n\n\n<li>Entries not accessed for over seven days are subject to removal.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub provides these illustrative monthly charges for fully utilized cache capacity:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Total configured and used cache<\/th><th>Illustrative monthly excess-storage charge<\/th><\/tr><tr><td>10 GB<\/td><td>$0<\/td><\/tr><tr><td>50 GB<\/td><td>$2.80<\/td><\/tr><tr><td>200 GB<\/td><td>$13.30<\/td><\/tr><tr><td>1,000 GB<\/td><td>$69.30<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Actual billing depends on usage over time and the applicable billing configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These are GitHub cache-storage charges only. They do not include AWS compute, networking, ECR, or other services.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A previously announced per-minute platform fee for self-hosted GitHub Actions was postponed. GitHub&#8217;s current billing documentation continues to state that self-hosted runner execution is free of GitHub Actions usage-minute charges. Organizations still pay for the infrastructure hosting their runners.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">23. AWS Cost Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For ARC on EKS, consider the following cost categories.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>AWS component<\/th><th>Cost driver<\/th><th>Optimization opportunity<\/th><\/tr><tr><td>EKS<\/td><td>Kubernetes cluster control plane<\/td><td>Avoid unnecessary test clusters<\/td><\/tr><tr><td>EC2 \/ EKS Auto Mode compute<\/td><td>Runner and controller node capacity<\/td><td>Match instances to workload and use autoscaling<\/td><\/tr><tr><td>EBS<\/td><td>Node and persistent volume storage<\/td><td>Avoid unnecessarily large volumes<\/td><\/tr><tr><td>NAT Gateway<\/td><td>Hourly and processed-data charges<\/td><td>Analyze traffic and use relevant VPC endpoints<\/td><\/tr><tr><td>Amazon ECR<\/td><td>Stored images and related usage<\/td><td>Manage image\/cache retention<\/td><\/tr><tr><td>Amazon S3<\/td><td>Object storage and requests<\/td><td>Lifecycle rules and appropriate storage classes<\/td><\/tr><tr><td>CloudWatch<\/td><td>Logs, metrics, and retention<\/td><td>Use deliberate log retention policies<\/td><\/tr><tr><td>EFS, if used<\/td><td>Shared filesystem usage<\/td><td>Avoid without a measured need<\/td><\/tr><tr><td>RDS, if unnecessarily added<\/td><td>Database compute and storage<\/td><td>Do not introduce for normal CI caching<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For private-subnet runners, ECR interface endpoints and an S3 gateway endpoint can reduce the need to send eligible AWS service traffic through a NAT Gateway.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub Actions traffic still requires an appropriate path to GitHub&#8217;s external services.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">VPC endpoints also have their own costs, so evaluate the total rather than assuming endpoints always save money.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">24. How to Benchmark Your Cache<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A reliable evaluation should test three scenarios:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Cold cache \u2014 no reusable cache data.<\/li>\n\n\n\n<li>Warm cache \u2014 exact cache hit.<\/li>\n\n\n\n<li>Invalidated cache \u2014 dependency or Dockerfile changes invalidate some cached content.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Measure:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Overall workflow duration<\/li>\n\n\n\n<li>Queue-to-runner-start delay<\/li>\n\n\n\n<li>Dependency installation duration<\/li>\n\n\n\n<li>Docker build duration<\/li>\n\n\n\n<li>Cache restore duration<\/li>\n\n\n\n<li>Cache upload duration<\/li>\n\n\n\n<li>Cache hit rate<\/li>\n\n\n\n<li>Cache archive size<\/li>\n\n\n\n<li>Runner CPU and memory consumption<\/li>\n\n\n\n<li>EKS compute and networking charges<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For a simple shell-command timing example:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-36\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">- name: Measure dependency installation\n  <span class=\"hljs-attr\">shell<\/span>: bash\n  <span class=\"hljs-attr\">run<\/span>: |\n    START=$(date +%s)\n\n    npm ci\n\n    END=$(date +%s)\n    DURATION=$((END - START))\n\n    echo <span class=\"hljs-string\">\"npm ci duration: ${DURATION} seconds\"<\/span>\n    echo <span class=\"hljs-string\">\"npm ci duration: ${DURATION} seconds\"<\/span> &gt;&gt; <span class=\"hljs-string\">\"$GITHUB_STEP_SUMMARY\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-36\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Collect results over enough executions to account for runner cold starts, varying network conditions, and cache hit\/miss patterns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A comparison based on only one cold build and one warm build is not reliable evidence of sustained performance improvement.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">25. Security Best Practices<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Self-hosted GitHub runners execute code supplied by workflows. This makes runner security an essential part of the architecture.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Recommended production controls<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Prefer ephemeral runners.<\/strong> Avoid reusing contaminated workspaces or lingering credentials.<\/li>\n\n\n\n<li><strong>Isolate CI workloads.<\/strong> Use dedicated runner compute or a dedicated cluster for high-risk builds.<\/li>\n\n\n\n<li><strong>Minimize privileged Docker execution.<\/strong> Do not run privileged Docker builds alongside sensitive production services.<\/li>\n\n\n\n<li><strong>Use short-lived cloud credentials.<\/strong> Prefer GitHub OIDC or appropriately scoped EKS identities.<\/li>\n\n\n\n<li><strong>Apply least privilege.<\/strong> Restrict GitHub Apps, Kubernetes RBAC, and AWS IAM roles.<\/li>\n\n\n\n<li><strong>Protect cache integrity.<\/strong> Restrict cache writes from untrusted workflow events.<\/li>\n\n\n\n<li><strong>Protect secrets.<\/strong> Never include credentials in cache directories or image layers.<\/li>\n\n\n\n<li><strong>Control outbound traffic.<\/strong> Provide only the networking required by builds and package repositories.<\/li>\n\n\n\n<li><strong>Maintain logs and audit trails.<\/strong> Export ephemeral runner logs before pod deletion.<\/li>\n\n\n\n<li><strong>Keep runner images current.<\/strong> GitHub enforces minimum runner compatibility requirements and timely updates.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Important runner-version requirement<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub has introduced stronger self-hosted runner version enforcement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented minimum registration version is <code>2.329.0<\/code>, and runner versions must remain within the required update window to continue receiving jobs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For production, pin tested controller and runner versions, automate controlled upgrades, and monitor compatibility requirements.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not assume that pinning a runner image indefinitely is safe.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">EKS Auto Mode consideration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">ARC can benefit from EKS Auto Mode node provisioning, but security and compatibility still matter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Evaluate privileged Docker workloads separately, particularly if the cluster is also running production services. AWS&#8217;s recommendation to avoid Docker-in-Docker on shared application clusters remains relevant.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For more restrictive execution requirements, investigate dedicated nonprivileged build approaches and separate runner node pools.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">26. Monitoring and Troubleshooting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start with these commands:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-37\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">helm <span class=\"hljs-keyword\">list<\/span> -A<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-37\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-38\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> pods -n arc-systems\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n arc-runners\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n arc-docker<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-38\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-39\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> autoscalingrunnersets -A\nkubectl <span class=\"hljs-keyword\">get<\/span> ephemeralrunners -A<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-39\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">To inspect an individual pod:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">kubectl describe pod POD_NAME -n arc-runners\nkubectl logs POD_NAME -n arc-runners<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">To inspect controller logs, first identify the actual controller pod or deployment name.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-40\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> deployments -n arc-systems\nkubectl logs -n arc-systems deployment\/CONTROLLER_DEPLOYMENT_NAME<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-40\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Troubleshooting table<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Problem<\/th><th>Likely cause<\/th><th>What to investigate<\/th><\/tr><tr><td>Workflow remains queued<\/td><td>Incorrect <code>runs-on<\/code>, unavailable listener, or runner capacity exhausted<\/td><td>Runner scale-set name, GitHub runner availability, listener logs<\/td><\/tr><tr><td>Runner pods remain Pending<\/td><td>Insufficient resources, scheduling constraints, or node provisioning failure<\/td><td><code>kubectl describe pod<\/code>, node status, capacity<\/td><\/tr><tr><td>Listener cannot authenticate<\/td><td>Invalid GitHub App permissions, installation ID, or secret<\/td><td>Authentication secret and controller\/listener logs<\/td><\/tr><tr><td>Docker daemon unavailable<\/td><td>Runner not configured for Docker mode or Docker sidecar failed<\/td><td><code>containerMode<\/code>, runner pod containers and logs<\/td><\/tr><tr><td>Privileged pod rejected<\/td><td>Admission or pod-security policies<\/td><td>Namespace policies and cluster security configuration<\/td><\/tr><tr><td>GitHub cache miss<\/td><td>Changed key, branch scope, expiration, or eviction<\/td><td>Key calculation, cache listings, workflow logs<\/td><\/tr><tr><td>Cache export denied<\/td><td>Insufficient cache write permission or low-trust workflow context<\/td><td><code>cache-mode<\/code>, trigger type, export settings<\/td><\/tr><tr><td>ECR push returns AccessDenied<\/td><td>Incorrect IAM permissions or AWS role trust<\/td><td>AWS identity, ECR policy, OIDC claims<\/td><\/tr><tr><td>Docker rebuilds every layer<\/td><td>Poor Dockerfile ordering or missing remote cache<\/td><td>Dockerfile, cache import\/export references<\/td><\/tr><tr><td>Runner registration rejected<\/td><td>Outdated runner binary<\/td><td>Runner image version and GitHub version enforcement<\/td><\/tr><tr><td>Kubernetes mode rejects a job<\/td><td>Job container is required by that mode<\/td><td><code>container:<\/code> configuration and hook settings<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For GitHub cache management, the GitHub CLI can also help inspect stored entries:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-41\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">gh cache <span class=\"hljs-keyword\">list<\/span> --repo YOUR_ORG\/YOUR_REPO<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-41\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Review retention, cache sizes, branch scope, and current usage before changing limits.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Production upgrade caution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">ARC upgrades can involve Kubernetes CRD compatibility changes. Do not assume that upgrading the chart is always equivalent to upgrading an ordinary stateless application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Review the upgrade procedure for the specific source and target releases, test in staging, preserve configuration, and plan for runner scheduling disruption.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not blindly delete ARC CRDs or uninstall production scale sets without confirming their impact.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 7 \u2014 Recommended Production Architecture<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">27. The Best Caching Architecture for ARC on EKS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For most organizations running mixed CI\/CD workloads, a layered approach is more practical than trying to standardize every build on one cache service.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Recommended design<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-42\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">flowchart TD\n    GH&#91;<span class=\"hljs-string\">\"GitHub Actions Workflows\"<\/span>] --&gt; TYPE{<span class=\"hljs-string\">\"Workflow type?\"<\/span>}\n\n    TYPE --&gt;|Node, Python, Maven| NORMAL&#91;<span class=\"hljs-string\">\"Native ARC Runner\"<\/span>]\n    TYPE --&gt;|Gradle <span class=\"hljs-keyword\">or<\/span> Android| GRADLE&#91;<span class=\"hljs-string\">\"Gradle-Optimized ARC Runner\"<\/span>]\n    TYPE --&gt;|Docker images| DOCKER&#91;<span class=\"hljs-string\">\"Isolated Docker Build Runner\"<\/span>]\n    TYPE --&gt;|Large test assets| DATA&#91;<span class=\"hljs-string\">\"Data Processing Runner\"<\/span>]\n\n    NORMAL --&gt; GHC&#91;<span class=\"hljs-string\">\"GitHub Dependency Cache\"<\/span>]\n    GRADLE --&gt; GC&#91;<span class=\"hljs-string\">\"Gradle Cache + Prepared Toolchain\"<\/span>]\n    DOCKER --&gt; ECR&#91;<span class=\"hljs-string\">\"Amazon ECR Registry Cache\"<\/span>]\n    DATA --&gt; S3&#91;<span class=\"hljs-string\">\"Amazon S3 Object Storage\"<\/span>]\n\n    GHC --&gt; OUT&#91;<span class=\"hljs-string\">\"Faster Repeated Builds\"<\/span>]\n    GC --&gt; OUT\n    ECR --&gt; OUT\n    S3 --&gt; OUT\n\n    RDS&#91;<span class=\"hljs-string\">\"RDS PostgreSQL\"<\/span>] -.-&gt; NO&#91;<span class=\"hljs-string\">\"Not a general CI cache backend\"<\/span>]\n\n    style GH fill:<span class=\"hljs-comment\">#dbeafe,color:#172554<\/span>\n    style OUT fill:<span class=\"hljs-comment\">#d1fae5,color:#064e3b<\/span>\n    style NO fill:<span class=\"hljs-comment\">#fee2e2,color:#7f1d1d<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-42\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">A reasonable starting configuration is:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>Workload<\/th><th>ARC configuration<\/th><th>Recommended cache<\/th><\/tr><tr><td>Node.js applications<\/td><td>Native Linux runner<\/td><td><code>actions\/setup-node<\/code> built-in cache<\/td><\/tr><tr><td>Python applications<\/td><td>Native Linux runner<\/td><td><code>actions\/setup-python<\/code> built-in cache<\/td><\/tr><tr><td>Maven applications<\/td><td>Native Linux runner<\/td><td><code>actions\/setup-java<\/code> Maven cache<\/td><\/tr><tr><td>Gradle and Android applications<\/td><td>Appropriately sized or preconfigured runner<\/td><td>Gradle official caching, selected provider<\/td><\/tr><tr><td>Docker builds<\/td><td>Isolated Docker-capable runner<\/td><td>ECR registry cache<\/td><\/tr><tr><td>Lightweight Docker builds<\/td><td>Isolated Docker-capable runner<\/td><td>BuildKit GitHub Actions cache<\/td><\/tr><tr><td>Large reference\/test assets<\/td><td>Appropriate isolated runner<\/td><td>S3 with integrity and lifecycle controls<\/td><\/tr><tr><td>Integration tests using PostgreSQL<\/td><td>Runner with suitable test-service support<\/td><td>Disposable PostgreSQL instance or test service, not a build cache<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">28. Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Does ARC require PostgreSQL, Redis, or another external database?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. The normal ARC architecture uses GitHub Actions services and Kubernetes resources. It does not require an external PostgreSQL or Redis database for runner scheduling or registration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can standard GitHub Actions Cache be used with EKS runners?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Ephemeral self-hosted runners can use the GitHub Actions cache service, provided they have compatible runner software, required tools, and network access.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does setting minRunners to zero make EKS completely free when idle?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. It eliminates the minimum idle runner capacity, but the controller, listener, EKS control plane, and any remaining worker node resources still consume infrastructure.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can ECR replace GitHub Actions Cache entirely?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not directly. ECR registry caching is an excellent solution for Docker build layers, but it is not a native replacement for dependency directories managed by <code>actions\/cache<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is S3 better than GitHub Actions Cache?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not universally. S3 offers scalable object storage and AWS integration, but implementing dependency caching around it requires additional tooling and maintenance.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use S3 when data size, reuse patterns, or storage requirements justify the effort.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is Redis faster than GitHub Actions Cache?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Redis can offer low-latency access for suitable key\/value workloads, but CI dependency archives are typically large filesystem objects. Redis is not automatically a better or cheaper solution for them.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Would PostgreSQL cache improve Docker builds?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No native integration provides that capability. Use a BuildKit-compatible external cache, such as the GitHub Actions cache or a container registry, instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I choose GitHub-hosted runners or ARC?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">GitHub-hosted runners generally have lower operational overhead.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ARC becomes more attractive when you need custom compute, private AWS connectivity, specific security boundaries, specialized toolchains, or enough workload volume to justify managing the infrastructure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Compare total cost and operational responsibility, not only per-minute compute prices.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Actions Runner Controller provides a scalable way to operate GitHub Actions self-hosted runners using Kubernetes and Amazon EKS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The greatest benefit is not simply running GitHub Actions inside Kubernetes. It is combining ephemeral execution, controlled infrastructure, dynamic runner capacity, and reusable build state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The most effective caching strategy depends on the workload. Native GitHub Actions caching should be the starting point for dependency-based builds. Docker builds benefit from BuildKit external caching, with ECR being a strong option for AWS-centric systems. S3 and shared storage should be reserved for use cases that justify their added complexity.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL is not a suitable general-purpose replacement for these cache mechanisms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A production-ready ARC deployment should prioritize isolation, minimum permissions, controlled runner updates, accurate performance measurement, and predictable infrastructure costs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The goal is not to cache everything. It is to reuse the right work, securely, when the cost of restoring it is lower than the cost of recreating it.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Official Documentation and Research References<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following primary sources were consulted and cross-checked:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/concepts\/runners\/actions-runner-controller\">GitHub \u2014 Actions Runner Controller overview<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/tutorials\/use-actions-runner-controller\/get-started\">GitHub \u2014 Get started with ARC<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/how-tos\/manage-runners\/use-actions-runner-controller\/deploy-runner-scale-sets\">GitHub \u2014 Deploying runner scale sets<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/reference\/workflows-and-actions\/dependency-caching\">GitHub \u2014 Dependency caching reference<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.github.com\/en\/billing\/concepts\/product-billing\/github-actions\">GitHub \u2014 Actions billing<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/actions\/actions-runner-controller\/releases\">GitHub \u2014 ARC releases<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.docker.com\/build\/cache\/backends\/\">Docker \u2014 Cache storage backends<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.docker.com\/build\/cache\/backends\/gha\/\">Docker \u2014 GitHub Actions cache backend<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.docker.com\/build\/ci\/github-actions\/cache\/\">Docker \u2014 Cache management with GitHub Actions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.aws.amazon.com\/AmazonECR\/latest\/userguide\/vpc-endpoints.html\">AWS \u2014 Amazon ECR VPC endpoints<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/best-practices\/pod-security.html\">AWS \u2014 Amazon EKS security best practices<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/github.blog\/changelog\/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners\/\">GitHub \u2014 Self-hosted runner version enforcement<\/a><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Research verification: October 27, 2026. The configuration examples are reference implementations derived from current documentation, not a claim that they have been executed in a live AWS account. Validate them in staging before production deployment.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>GitHub Actions is a CI\/CD platform that automates tasks such as building applications, running tests, creating Docker images, and deploying software. Every GitHub Actions job requires a&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-79582","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79582","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=79582"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79582\/revisions"}],"predecessor-version":[{"id":79583,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79582\/revisions\/79583"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=79582"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=79582"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=79582"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}